使用加号符号进行字符串拼接

3
今天我阅读了Antonio关于toString()性能的博客,其中有一段话:

昨天被认为是邪恶的(“不要使用+连接字符串!!!”),今天已经变得酷和高效!现在JVM将+符号编译成一个字符串构建器(在大多数情况下)。所以,请毫不犹豫地使用它。

现在我感到困惑了,因为他说现在JVM将+符号编译成一个字符串构建器(在大多数情况下),但我从未听说过或看到过(代码)类似的东西。
请问是否可以举例说明JVM何时这样做以及在什么情况下会发生

可能是重复问题:https://dev59.com/JnVD5IYBdhLWcg3wOo9h - Eric
@Eric,恐怕这与上面提到的问题无关。因为在这个问题中,他明确说明了concat()方法只接受字符串值,而加号运算符将自动将参数转换为字符串(对于对象使用toString()方法)。然而,我所说的是发生在StringBuilder中的转换。如果有遗漏,请告知我。 - Mehraj Malik
2
@MehrajMalik,你看过被接受的答案吗?那几乎是一个完美的重复。 - SomeJavaGuy
3
是的,我做到了。他提到StringBuilder转换发生在加号运算符后面。然而,我的问题是这是否总是发生,还是需要某些特定条件。正如博客所述(在大多数情况下会发生)。那么,在哪些情况下它不会转换为StringBuilder? - Mehraj Malik
显示剩余3条评论
4个回答

14
规则是错误的,因为它不完整,从而具有误导性。
规则是:在循环中不要使用+连接字符串。
这个规则仍然有效。最初的规则从未打算在循环之外应用!
一个简单的循环。
String s = "";
for (int i = 0; i < 10000; i++) { s += i; }
System.out.println(s);

仍然比较慢,比较慢。
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) { sb.append(i); }
System.out.println(sb.toString());

因为Java编译器必须将第一个循环转换为...
String s = "";
for (int i = 0; i < 1000; i++) { s = new StringBuilder(s).append(i).toString(); }
System.out.println(s);

此外,该声明至少是误导性的,因为这种转换在Java 1.0中已经完成了(好吧,不是使用StringBuilder,而是使用StringBuffer,因为StringBuilder是在Java5中才添加的)。
有人也可以争辩说,这个说法是错误的,因为编译并不是由JVM完成的,而是由Java编译器完成的。
对于这个问题:Java编译器何时使用StringBuilder.append(),何时使用其他机制?
Java编译器的源代码(版本1.8)中有两个地方处理通过+运算符进行字符串连接。 结论是对于OpenJDK的Java编译器(也就是由Oracle分发的编译器),短语“在大多数情况下”意味着总是。(尽管这可能会在Java 9中改变,或者可能是Eclipse内置的另一个Java编译器使用了其他机制)。

1
“因为编译不是由JVM完成的”,哈哈,很好的发现。然而,JLS 15.18.1指出,“Java编译器可以使用StringBuffer类”。但是,它没有说明在什么情况下不使用它。 - dumbPotato21
@ChandlerBing 和 Thomas(关于JVM编译的惊人发现),没错。没有人提到它不使用StringBuilder进行连接的条件是什么。 - Mehraj Malik
根据https://docs.oracle.com/javase/8/docs/api/java/lang/String.html,“Java语言为字符串连接运算符(+)和将其他对象转换为字符串提供了特殊支持。字符串连接是通过StringBuilder(或StringBuffer)类及其append方法实现的。字符串转换是通过由Object定义并由Java中的所有类继承的toString方法实现的。有关字符串连接和转换的其他信息,请参见Gosling,Joy和Steele,Java语言规范。” - Eric
2
@MehrajMalik,目前我只知道字符串连接有两种情况:要么两个操作数都是常量字符串(然后编译器将其替换为字符串字面量),要么至少有一个操作数不是常量字符串(然后编译器使用StringBuilder)。但我会尝试查看Java编译器是否还有其他情况。 - Thomas Kläger
由于非常量值的连接代码取决于特定的编译器,因此不可能说所有编译器都这样做,因为这意味着我们声称了解所有编译器。我们可以说,所有相关的编译器,即javac和ecj,始终使用优化策略。顺便说一句,Java 9的javac将不使用StringBuilder,但是,这是因为新策略被认为更好... - Holger
@MehrajMalik 我已经在我的答案中更新了我在Java编译器源代码中找到的信息。 - Thomas Kläger

5

Holger在他的评论中提到,在Java-9中,字符串连接的+将从StringBuilder更改为由JRE通过invokedynamic选择的策略。 JDK-9中可能有6种String concatenation策略:

  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
}

默认的方法并不是使用StringBuilder,而是使用MH_INLINE_SIZED_EXACT。实际上,这个实现方式相当疯狂,并且试图进行高度优化。

所以,根据我所知道的,那个建议是错误的。顺便说一下,这是由Aleksey Shipilev在jdk中主要努力的方向。他还在jdk-9中对String内部做了一个重大改变,现在它们是由byte[]支持而不是char[]。这是必需的,因为ISO_LATIN_1字符串可以用单个字节(一个字符 - 一个字节)编码,所以需要更少的空间。


4
这个陈述以其精确的形式是错误的,它符合链接博客继续写一些无稽之谈的情况,比如你必须用 Objects.toString(…) 包装引用来处理 null,例如 "att1='" + Objects.toString(att1) + '\'' 而不是只用 "att1='" + att1 + '\''。没有必要这样做,显然,作者从未重新检查过这些声明。
JVM 不负责编译 + 运算符,因为这个运算符只是源代码工件。编译器,例如 javac 才是负责的,虽然编译后的形式没有保证,但编译器被鼓励使用构建器,由 Java 语言规范 确定。
一种实现可能选择在一步中执行转换和连接,以避免创建然后丢弃中间的String对象。为了增加重复字符串连接的性能,Java编译器可以使用StringBuffer类或类似的技术来减少通过表达式求值创建的中间String对象的数量。
请注意,即使编译器不执行此优化,字节码级别上仍不存在“+”运算符,因此编译器必须选择一个JVM理解的操作,例如使用String.concat,在只连接两个字符串的情况下,这可能比使用StringBuilder更快。
即使假设最坏的字符串连接编译策略(仍然在规范内),也不能说永远不要使用“+”连接字符串,因为当您定义编译时常量时,使用“+”是唯一的选择,当然,编译时常量通常比在运行时使用StringBuilder更有效率。
实际上,在 Java 5 之前,应用于非常量字符串的 + 操作符编译成了 StringBuffer 的使用方式,而在 Java 5 到 Java 8 中则编译成了 StringBuilder 的使用方式。当编译后的代码与手动使用 StringBuffer 或 StringBuilder 的代码相同时,其性能不会有差异。
超过十年以前切换到 Java 5 时,通过 + 进行字符串连接首次明显优于手动使用 StringBuffer,因为只需重新编译连接代码即可使其内部使用可能更快的 StringBuilder,而手动处理 StringBuffer 的代码则需要重写以使用在该版本中引入的 StringBuilder。
同样地,Java 9 将使用 invokedynamic 指令来编译字符串连接,允许 JRE 将其绑定到执行操作的实际代码,包括在普通 Java 代码中不可能进行的优化。因此,只需重新编译字符串连接代码即可获得此功能,而无法手动使用等效内容。

话虽如此,尽管字符串拼接从未被视为邪恶的前提是错误的,但建议是正确的,不要犹豫地使用它。

只有在需要大量初始容量或在循环内大量连接且该代码已被性能分析工具确认为实际性能瓶颈的情况下,您才真正可能通过手动处理缓冲区来提高性能...


0
当你使用+运算符连接字符串时,编译器会将连接代码转换为使用StringBuffer以提高性能。为了改善性能,StringBuffer是更好的选择。
使用+运算符是连接两个字符串的最快方式。
String str = "Java";
str = str + "Tutorial";

编译器将此代码翻译为:
String s1 = "Java";
StringBuffer sb = new StringBuffer(s1);
sb.append("Tutorial");
s1 = sb.toString();

因此,在连接字符串时最好使用 StringBufferString.format

使用 String.format

String s = String.format("%s %s", "Java", "Tutorial");

3
这并没有回答问题。OP问的是编译器什么时候不会+翻译为append - dumbPotato21

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