我尝试过这个例子来发现 StringBuffer、StringBuilder 和 String 的执行时间不同
经过尝试,我发现 StringBuffer 和 StringBuilder 花费的时间更少,因为它没有创建新对象。
作为字符串追加空字符串也不会创建任何对象,因此速度更快。
当我 append 一些字符串时,应该需要更多时间,因为创建对象需要时间。
当我对另一个字符串执行相同的 append 字符串模式时,这也花费了更多时间。在这种情况下,所有对象都已在字符串池中可用。为什么它花费的时间和以前一样?
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
如有错误,请指正。
最佳答案
鉴于您的最后两个测试是相同的,因此您实际上只有四个测试。为了方便起见,我将它们重构为单独的方法并删除了基准测试代码,因为没有必要了解这里发生的情况。
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的父类(super class)是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)
,它执行更多数组复制。数组复制的确切实现隐藏在 native 代码中,因此我不会四处寻找它们。
更复杂的是,我们创建然后丢弃的对象的绝对数量可能足以触发 JVM 垃圾收集器的运行,这会带来进一步的开销。
总而言之;相当于 nonEmptyStringConcatTest
的性能大幅下降很大程度上取决于编译器所做的糟糕的“优化”。通过不在循环内进行直接串联来避免这种情况。
关于java - 即使字符串池中已有可用的对象,字符串追加也会花费更多时间,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25305933/