即使字符串池中已有的对象,字符串追加也需要更多时间

时间:2014-08-14 10:33:34

标签: java string append stringbuilder stringbuffer

我试过这个例子来查找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

如果我在任何地方都错了,请纠正我。

1 个答案:

答案 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没有的不可避免的开销,因为没有同步。

因此,我们感兴趣的两种方法是emptyStringConcatTestnonEmptyStringConcatTest。如果我们检查编译版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的源代码,可以进一步解释您在nonEmptyStringConcatTestStringBuilder#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的性能下降很大程度上取决于编译器所做的糟糕“优化”。从不在循环中直接连接来避免它。