我试过这个例子来查找StringBuffer,StringBuilder和String
的执行时间差异经过尝试,我知道StringBuffer和StringBuilder花费的时间更少,因为它没有创建新对象。
作为带有空字符串的字符串追加,也不会创建任何对象,因此速度更快。
当我使用某个字符串追加时,它应该花费更多时间,因为创建对象需要时间。
当我做同样的追加字符串模式与另一个字符串时,也花费更多的时间。在这种情况下,所有对象都已在String池中可用。为什么它和以前一样?
public class StringComparation {
public static void main(String[] args) {
int N = 100000;
long time;
// String Buffer
StringBuffer sb = new StringBuffer();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
sb.append("a");
}
System.out.println("String Buffer - " + (System.currentTimeMillis() - time));
// String Builder
StringBuilder sbr = new StringBuilder();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
sbr.append("a");
}
System.out.println("String Builder - " + (System.currentTimeMillis() - time));
// String Without String pool value
String s2 = new String();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
s2 = s2 + "";
}
System.out.println("String Without String pool value - "
+ (System.currentTimeMillis() - time));
// String With new String pool Object
String s = new String();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
s = s + "a";
}
System.out.println("String With new String pool Object - "
+ (System.currentTimeMillis() - time));
// String With already available String pool Object
String s1 = new String();
time = System.currentTimeMillis();
for (int i = N; i --> 0 ;) {
s1 = s1 + "a";
}
System.out.println("String With already available String pool Object - "
+ (System.currentTimeMillis() - time));
}
}
输出:
String Buffer - 43
String Builder - 16
String Without String pool value - 64
String With new String pool Object - 12659
String With already available String pool Object - 14258
如果我在任何地方都错了,请纠正我。
答案 0 :(得分:1)
鉴于你的最后两个测试是相同的,你真的只有四个测试。为了方便起见,我将它们重构为单独的方法并删除了基准测试代码,因为没有必要了解这里发生了什么。
public static void stringBuilderTest(int iterations) {
final StringBuilder sb = new StringBuilder();
for (int i = iterations; i-- > 0;) {
sb.append("a");
}
}
public static void stringBufferTest(int iterations) {
final StringBuffer sb = new StringBuffer();
for (int i = iterations; i-- > 0;) {
sb.append("a");
}
}
public static void emptyStringConcatTest(int iterations) {
String s = new String();
for (int i = iterations; i-- > 0;) {
s += "";
}
}
public static void nonEmptyStringConcatTest(int iterations) {
String s = new String();
for (int i = iterations; i-- > 0;) {
s += "a";
}
}
我们已经知道StringBuilder版本的代码是四个中最快的。 StringBuffer版本较慢,因为它的所有操作都是同步的,这带来了StringBuilder没有的不可避免的开销,因为没有同步。
因此,我们感兴趣的两种方法是emptyStringConcatTest
和nonEmptyStringConcatTest
。如果我们检查编译版emptyStringConcatTest
的字节码,我们会看到以下内容:
public static void emptyStringConcatTest(int);
flags: ACC_PUBLIC, ACC_STATIC
LineNumberTable:
line 27: 0
line 28: 8
line 29: 17
line 31: 40
Code:
stack=2, locals=3, args_size=1
0: new #14 // class java/lang/String
3: dup
4: invokespecial #15 // Method java/lang/String."<init>":()V
7: astore_1
8: iload_0
9: istore_2
10: iload_2
11: iinc 2, -1
14: ifle 40
17: new #7 // class java/lang/StringBuilder
20: dup
21: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
24: aload_1
25: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: ldc #16 // String
30: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto 10
40: return
LineNumberTable:
line 27: 0
line 28: 8
line 29: 17
line 31: 40
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 10
locals = [ class java/lang/String, int ]
frame_type = 250 /* chop */
offset_delta = 29
在引擎盖下,这两种方法几乎完全相同,这条线是唯一的区别:
空字符串:
28: ldc #9 // String
非空字符串(注意小但重要的差异!):
28: ldc #9 // String a
关于字节码的首要注意事项是for
循环体的结构:
10: iload_2
11: iinc 2, -1
14: ifle 40
17: new #7 // class java/lang/StringBuilder
20: dup
21: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
24: aload_1
25: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: ldc #16 // String
30: invokevirtual #10 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto 10
我们实际上最终得到的是编译器优化已转为
for (int i = iterations; i-- > 0;) {
s += "";
}
成:
for (int i = iterations; i-- > 0;) {
s = new StringBuilder().append(s).append("").toString();
}
那不好。我们在每次迭代中实例化一个新的临时StringBuilder对象,其中有100,000个。这是很多对象。
如果我们检查emptyStringConcatTest
的源代码,可以进一步解释您在nonEmptyStringConcatTest
和StringBuilder#append(String)
之间看到的差异:
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuilder的超类是AbstractStringBuilder,让我们来看看append(String)
的实现:
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
if (len == 0) return this;
int newCount = count + len;
if (newCount > value.length)
expandCapacity(newCount);
str.getChars(0, len, value, count);
count = newCount;
return this;
}
你会注意到,如果参数str
的长度为零,则该方法只返回而不进行任何进一步的操作,在空字符串的情况下非常快。
非空字符串参数触发对后备char[]
的边界检查,可能导致expandCapacity(int)
调整其大小,将原始数组复制到新的更大的数组中(请注意后台StringBuilder中的数组不是final
- 可以重新分配!)。完成后,我们会调用String#getChars(int, int, char[], int)
,这会进行更多数组复制。数组复制的确切实现隐藏在本机代码中,所以我不打算去寻找它们。
为了进一步复合,我们正在创建然后丢弃的大量对象可能足以触发JVM的垃圾收集器的运行,这会带来进一步的开销。
总结如此;相当于nonEmptyStringConcatTest
的性能下降很大程度上取决于编译器所做的糟糕“优化”。从不在循环中直接连接来避免它。