Javac何时引入StringBuilder/StringBuffer优化?

28

我知道Javac编译器能够使用StringBuilder/StringBuffer来转换字符串连接符“+”,我很好奇这个更改是从哪个版本开始引入的?

我正在使用以下示例代码:

public class Main {
  public static void main(String[] args) {
      String a = args[0];
      String s = "a";
      s = s + a;
      s = s + "b";
      s = s + "c";
      s = s + "d";
      s = s + "e";
      System.out.println(s);
  }
}

到目前为止,我已经尝试使用 javac 1.8.0_121javac 1.6.0_20javac 1.5.0_22java 1.4.2_19

这里是我使用来自 1.4.2_19javap -c 看到的字节码示例:

6:  astore_2
7:  new #3; //class StringBuffer
10: dup
11: invokespecial   #4; //Method java/lang/StringBuffer."<init>":()V
14: aload_2
15: invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
18: aload_1
19: invokevirtual   #5; //Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
22: invokevirtual   #6; //Method java/lang/StringBuffer.toString:()Ljava/lang/String;

所有4个版本似乎都在使用StringBuilder/StringBuffer优化,所以我很想知道从哪个Javac版本开始引入了这个更改?


可能自从开始以来...... - M A
如果我没记错的话,是Java 5。 - Maurice Perry
更有用的是要知道它们中是否有任何一个可以处理由循环构建的字符串。 - Boann
1
请注意,编译器并不总是自动优化代码。去年我使用了String#+而非StringBuilder,导致我的Linux服务器和Java 8崩溃。我想要构建一个相当大(约300MB)的gnuplot文件。这其中涉及到一些逻辑,我需要同时构建两个150MB的字符串,并在最后将它们连接起来。使用String#+花费了半个小时并且耗尽了所有可用内存。而使用StringBuilder只需几秒钟并且占用更少的内存。 - Eric Duminil
4个回答

32
这里有一段来自版本1的语言规范的引用:
实现可以选择执行转换和连接操作以避免创建和丢弃中间String对象。为了提高重复字符串连接的性能,Java编译器可以使用StringBuffer类(§20.13)或类似技术来减少通过表达式的评估所创建的中间String对象的数量。
当时,他们使用的是StringBuffer而不是StringBuilder
再来一段来自JDK1.0.2的StringBuffer引用:
此类是一个可增长的字符缓冲区,主要用于创建字符串。编译器使用它来实现“+”运算符。

2
不错的发现,但现在我必须鄙视我的前任老师教我使用StringBuffer/Builder这种冗长无用的语法;-( 我会试着找到一些安慰,相信在早期版本的OpenJDK中可能没有被优化过,请不要破坏这个想法 :p - Aaron
11
通常在构建复杂字符串时(例如在循环中),应该使用 StringBuilder(请参见https://dev59.com/JnI_5IYBdhLWcg3wFu_L)。 - M A
1
哦,感谢您的精确解答!虽然这不是问题的重点,但我认为将其添加到您的答案中仍然很有趣,因为提问者和其他读者可能会忽略这种优化并得出与我错误相同的结论,即在任何地方使用字符串连接都是可以的。 - Aaron
1
@Aaron 我不会称之为过早优化。如果一个循环执行了10000次的字符串连接操作,使用 + 操作符与使用默认缓冲区大小为16个字符的 StringBuilder 是同样愚蠢的。但有一种情况,使用 + 操作符实际上更快,那就是在连接静态字符串值时。在这种情况下,结果字符串可以在编译时构建。 - toniedzwiedz
不需要深入研究JLS或字节码...只需查看StringBuilder的Javadoc!它说自1.5版本以来。因此,这个答案不是最好的答案。 - Bludzee
显示剩余6条评论

11

我查看了Java语言规范的第一版(1996年)。虽然不容易找到,但在这里可以找到。即使在那时,关于字符串拼接优化的部分也是存在的:

实现可能选择执行转换和连接以避免创建并丢弃中间的String对象。为了提高重复字符串连接的性能,Java编译器可以使用StringBuffer类(§20.13)或类似技术来减少通过表达式求值创建的中间String对象的数量。

该规范指的是StringBuffer,但当前JLS的措辞则认为StringBuilder更好,因为它的方法没有同步。

然而,这并不意味着应该始终依赖这种优化。例如,在循环中进行字符串连接将不会得到优化。


5

不需要深入研究JLS或字节码...只需查看StringBuilder的Javadoc!它说“自1.5版本以来”。因此,这个答案是最好的答案。 (+1) - Bludzee

5

这并没有回答问题,但是我想补充一下总体观点,在jdk-9中,StringBuilder::append其中一种允许的策略,但不是默认策略。

private enum Strategy {
   /**
    * Bytecode generator, calling into {@link java.lang.StringBuilder}.
    */
    BC_SB,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but trying to estimate the required storage.
     */
    BC_SB_SIZED,

    /**
     * Bytecode generator, calling into {@link java.lang.StringBuilder};
     * but computing the required storage exactly.
     */
     BC_SB_SIZED_EXACT,

   /**
    * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
    * This strategy also tries to estimate the required storage.
    */
    MH_SB_SIZED,

    /**
     * MethodHandle-based generator, that in the end calls into {@link java.lang.StringBuilder}.
     * This strategy also estimate the required storage exactly.
     */
    MH_SB_SIZED_EXACT,

    /**
     * MethodHandle-based generator, that constructs its own byte[] array from
     * the arguments. It computes the required storage exactly.
     */
     MH_INLINE_SIZED_EXACT
}

实际上,这是一个用于字符串连接的invokedynamic字节码,因此它的实现现在是基于JRE的具体实现,而不是编译器。默认策略是:MH_INLINE_SIZED_EXACT


抱歉,您能解释一下“允许的策略”是什么意思吗?这是一种专门用于字符串拼接的运行时优化吗? - glee8e
@glee8e 我认为我在这里已经涵盖了这个话题:https://dev59.com/UlkS5IYBdhLWcg3wKzuq#42138460 - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接