java - 即使字符串池中已有可用的对象,字符串追加也会花费更多时间

标签 java string append stringbuilder stringbuffer

我尝试过这个例子来发现 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 没有的不可避免的开销,因为它同步。

所以我们感兴趣的两个方法是 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 之间看到的差异和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/

相关文章:

c# - C# 中的串联

jquery - 使用 JQuery 从 AJAX 请求中破坏 HTML

Python - 在追加模式下打开文件。写永远不会发生

java - 如何从 javax.lang.model.VariableElement 获取参数类型

string - Delphi:调用名称存储在字符串中的函数

java - 在哪里可以找到 Java(swing、JWS)与 RIA 的 HTML/JS 的简明比较

java - 将整数显示为 Xs 的输出

Python:将二维列表 append 到另一个二维列表

java - Java拖拽时如何获取鼠标信息?

java - JButton 在其边框和按钮本身之间有填充