0%

优化项目中的字符串拼接

在项目中拼接字符串有很多种方式,使用不当的话很容易对性能造成影响,下面就讲讲入如何优化项目中的字符拼接代码。

优化 + 拼接操作

最常用的就是 + 操作了,+ 其实属于 Java 的语法糖:在编译阶段,编译器会将 + 操作编译为 StringBuilder,比如下面的代码:

1
2
3
4
5
String a = "Hello ";
String b = "World";
// 编译器优化后:String c = new StringBuilder().append(a).append(b);
String c = a + b;
System.out.println(c);

利用 javap 进行一下反编译,可以看到 String c = a + b 这行代码被编译为了 StringBuilderappend 操作:

hexo

因此,与 StringBuilder 一样,应该避免在代码中使用如下的字符拼接方式:

1
2
3
4
5
6
String a = "";
// 循环中拼接字符,会导致创建多个StringBuilder对象
for (int i = 0; i < 10; i++) {
a += i;
}
System.out.println(a);

上面这种优化方式是网上经常提到的,那么除了这种场景以外,项目中其他的 + 字符是否还需要优化呢?这个疑惑在这篇文章中找到了答案:java 字符串拼接的几种方式详解(执行效率及内存占用等对比)

由于编译器会帮助我们优化,那加号的字符串拼接操作可否认为等同 StringBuilder 的使用呢?

答案是这种认知是错误的,主要是以下两点原因:

  1. 如果加号拼接是多次分开操作的,其实相当于多次实例化了 StringBuilder 对象;
  2. StringBuilder 的构造方法有 4 个,加号拼接操作优化成调用 StringBuilder 的默认无参构造方法,和实际使用其它构造方法会有区别。

如果使用场景避免如上两个 case,那么其实两种方式是一样的。

结合上面的分析,说说我的个人理解。如果项目中的字符串拼接出现了如下的场景,那么需要使用 StringBuilder 来进行优化:

  1. 循环中使用 + 拼接字符串,或者 + 是分多次进行拼接的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 循环中拼接
    String a = "";
    for (int i = 0; i < 10; i++) {
    // 每次都会new一个StringBuilder
    a += i;
    }

    // 多次拼接
    String s1 = "A";
    String s2 = "B";
    // new一个StringBuilder
    String first = s1 + s2;
    String s3 = "C";
    // new第二个StringBuilder
    String second = first + s3;
  2. + 拼接的字符串长度很长,会导致多次扩容操作。那么需要使用带参的 StringBuilder 构造函数进行优化。
    1
    2
    3
    4
    String s1 = "sdfghjklzxcvnmsdjsfjksfsfsjf";
    String s2 = "sdjkjgdfjgldkdjldkjgldkgjdljkdgkdjgldjdglkdl";
    // StringBuilder无参构造函数默认长度是16,在append()时若长度不够会进行扩容
    String s3 = s1 + s2;

除上面说的两种情况外,其他的 + 拼接场景可以沿用。 + 本身就是 Java 的一个语法糖,如果性能不会受到影响,那就没有必要过度优化。

复用 StringBuilder 对象

另外,我们还可以利用 StringBuildersetLength(0)delete(0, length) 方法来优化循环中多次创建 StringBuilder 的代码,如下:

1
2
3
4
5
6
7
8
9
List<String> list = new LinkedList<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
// 重置指针,避免重复创建对象
sb.setLength(0);
sb.append(System.currentTimeMillis()).append("-").append(i);
list.add(sb.toString());
}
list.forEach(System.out::println);

setLength(0)delete(0, length) 都可以清空 String 对象,区别是 delete() 操作多执行了一次 cp 数组拷贝操作,而 setLength() 只是重置了 count,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// setLength()
public void setLength(int newLength) {
if (newLength < 0)
throw new StringIndexOutOfBoundsException(newLength);
ensureCapacityInternal(newLength);

if (count < newLength) {
Arrays.fill(value, count, newLength, '\0');
}

count = newLength;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// delete()
public AbstractStringBuilder delete(int start, int end) {
if (start < 0)
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) {
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
}

对比实验见:StringBuffer清空操作delete和setLength的效率对比分析

参考文章

StringBuilder你应该知道的几件事情

String、StringBuilder、StringBuffer

java 字符串拼接的几种方式详解(执行效率及内存占用等对比)

StringBuffer清空操作delete和setLength的效率对比分析