当前位置: 首页 > 知识库问答 >
问题:

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

夏俊杰
2023-03-14

我尝试了这个示例,以发现StringBuffer、StringBuilder和String在执行时间上的不同

经过尝试,我了解到StringBuffer和StringBuilder所用的时间更少,因为它不创建新对象。

作为字符串追加空字符串,也不创建任何对象,所以速度更快。

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个答案

景阳平
2023-03-14

鉴于您的最后两个测试是相同的,您实际上只有四个测试在这里。为了方便起见,我将它们重构为单独的方法,并删除了基准测试代码,因为没有必要理解这里发生了什么。

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();
}
public StringBuilder append(String str) {
    super.append(str);
    return this;
}
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;
}

非空字符串参数触发backingchar[]的边界检查,这可能会导致expandcapacity(int)调整其大小,从而将原始数组复制到一个新的、更大的数组中(请注意,StringBuilder中的backing数组不是final-它可以重新分配!)。完成后,我们调用string#getchars(int,int,char[],int),它执行更多的数组复制。数组复制的确切实现隐藏在本机代码中,所以我不打算到处寻找它们。

更复杂的是,我们创建并扔掉的对象数量可能足以触发JVM垃圾收集器的运行,这会带来更多的开销。

总之;与nonemptystringconcattest等价物性能的巨大下降很大程度上是由于编译器进行了糟糕的“优化”。不要在循环内直接串联,以避免这种情况。

 类似资料:
  • append key value 返回新字符串值的长度。

  • 在下面的Java代码中将创建多少对象:

  • 问题内容: 当我使用定义变量时,该字符串不会添加到字符串池中,对吗? 现在,当我定义另一个而不是时,我将其定义为突然。(或我的大学老师说)。这是为什么?是什么使这个字符串突然变成字符串池字符串? 问题答案: 当我用new定义一个StringBuffer变量时,该字符串没有添加到字符串池中,对吗? 创建a 根本不会创建a 。 现在,当我定义另一个StringBuffer而不是new时,我突然将其定义

  • 问题内容: 在每篇文章中,“如何向JEditorPane附加字符串?”问题的答案。就像 我已经试过了: 结果,我得到了“终止时间:1000”,而没有“进程的分布:” 为什么会发生这种情况??? 问题答案: 我怀疑这是附加文本的推荐方法。这意味着每次更改某些文本时,都需要重新解析整个文档。人们之所以这样做,是因为他们不了解如何使用JEditorPane。包括我在内。 我更喜欢使用JTextPane然

  • 被删除,因此也是另一个匹配字符串的一部分,不确定这是由于错误的regEx还是反字符类的错误应用。

  • 问题内容: 我正在尝试采用一个字符串,并将其附加到列表中包含的每个字符串中,然后使用完成的字符串创建一个新列表。例: 我尝试了循环,并尝试了列表理解,但这是垃圾。一如既往的任何帮助,不胜感激。 问题答案: 最简单的方法是使用列表理解: 请注意,我避免使用内置名称,因为那样会掩盖或隐藏内置名称,这非常不好。 另外,如果您实际上不需要列表,而只需要一个迭代器,则生成器表达式可能会更高效(尽管在短列表中