由于String是不可变对象(final),所以,对字符串进行连接、替换操作时,String对象总是会生成新的对象。所以连接和替换时性能很差。

String常量字符串的累加

比如我们使用如下代码进行字符串连接:

1
String str = "hello"+"world"+"!";

先有hello和world2个字符串生成helloworld,然后再生成helloworld!。

将上面的代码做5万次循环。
但上面的代码执行效率竟然比使用StringBuilder快,为什么呢?

1
2
3
4
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
sb.append("!");

对第一段代码进行反编译,可以看到对于常量字符串的累加,java在编译时就做了优化。

1
String str = "helloworld!";

String变量字符串的累加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
String a = "hello";
String b = "world";
String c = "!";
int loopCount = 50000000;
long s = System.currentTimeMillis();
for (int i=0;i<loopCount;i++) {
// test(a,b,c);
test2(a,b,c);
}
System.out.println("cost " + (System.currentTimeMillis() - s) + "ms.");
}
private static void test(String a,String b,String c) {
StringBuilder stringBuilder = new StringBuilder();
String s = stringBuilder.append(a).append(b).append(c).toString();
}
private static void test2(String a,String b,String c) {
String s = a + b + c;
}
}

同样做5万次循环,发现与使用StringBuilder性能差不多。
反编译,发现java在编译时做了优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
public Test() {
}
public static void main(String[] args) {
String a = "hello";
String b = "world";
String c = "!";
int loopCount = 50000000;
long s = System.currentTimeMillis();
for(int i = 0; i < loopCount; ++i) {
test2(a, b, c);
}
System.out.println("cost " + (System.currentTimeMillis() - s) + "ms.");
}
private static void test(String a, String b, String c) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(a).append(b).append(c).toString();
}
private static void test2(String a, String b, String c) {
(new StringBuilder()).append(a).append(b).append(c).toString();
}
}

可以看到,java在编译的时候将字符串的+操作转换成了使用StringBuilder的append方式。

构建超大的字符串

对下面的代码ABC分别执行10000次。
代码A:

1
2
3
4
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + i;
}

执行耗时:4542ms。

代码B:

1
2
3
4
String s = "";
for (int i = 0; i < 1000; i++) {
s = s.concat(String.valueOf(i));
}

执行耗时:4079ms。

代码C:

1
2
3
4
5
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 1000; i++) {
stringBuilder.append(i);
}
String s = stringBuilder.toString();

执行耗时:259ms。

可以看到,从快到慢依次是StringBuilder > String.concat() > String+。并且StringBuilder要快很多。

观察编译后的代码发现代码Ajava编译器并没有做任何优化。

选择StringBuilder还是StringBuffer?

StringBuffer与StringBuilder最大的不同在于,StringBuffer对几乎所有的方法都做了同步,StringBuilder没有做任何同步。
由于方法同步需要消耗一定的系统资源,因此,StringBuffer效率要低于StringBuilder。但是,在多线程环境中,StringBuilder无法保证线程安全,不能使用。

所以,如果是不需要考虑线程安全的情况下,使用StringBuilder;相反则使用StringBuffer。

容量参数

无论是StringBuffer还是StringBuilder都可以在初始化时设置一个容量参数。

1
2
public StringBuffer(int capacity);
public StringBuilder(int capacity);

在不指定容量参数时,默认是16个字符。
代码如下:

1
2
3
public StringBuilder() {
super(16);
}

StringBuffer和StringBuilder都继承自AbstractStringBuilder,AbstractStringBuilder的构造器代码:

1
2
3
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

不带容量参数测试

1
2
3
4
5
//StringBuilder sb = new StringBuilder();
StringBuffer sb = new StringBuffer();
for (int i=0;i<10000000;i++) {
sb.append(i);
}

StringBuilder耗时405ms,StringBuffer耗时557ms。

带容量参数测试

1
2
3
4
5
6
//StringBuilder sb = new StringBuilder(68888890);
StringBuffer sb = new StringBuffer(68888890);
for (int i=0;i<10000000;i++) {
sb.append(i);
}
//System.out.println(sb.length());

StringBuilder耗时330ms,StringBuffer耗时464ms。

通过对比,可以看到增加容量参数可以增加StringBuffer和StringBuilder的性能。

StringBuffer和StringBuilder在执行append方法时,实际是调用父类AbstractStringBuilder的append方法。
父类append方法定义如下:

1
2
3
4
5
6
7
8
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}

可以看到一个ensureCapacityInternal方法,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* This method has the same contract as ensureCapacity, but is
* never synchronized.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}

/**
* This implements the expansion semantics of ensureCapacity with no
* size check or synchronization.
*/
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}

minimumCapacity就是现在存储的数据的长度+本次append字符串的长度。如果该长度比定义的保存数据的char[]的长度大,说明char[]存储空间不够,需要进行扩容了。

扩容的逻辑:新的容量为目前数据的容量的2倍+2;如果扩容后的长度仍然小于目前数据的长度+本次append字符串的长度,则新的容量为目前数据的长度+本次append字符串的长度。
然后执行了一次数组的复制,将旧的数据复制到新的数组中。

所以,如果指定合适的容量,可以避免SringBuilder和StringBuffer的内存复制,这样可以提升append的性能。
这个跟HashMap比较像,HashMap在put数据时,也会在容量不够是进行扩容。

参考:《Java程序性能优化——让你的java程序更快、更稳定》