【问题标题】:String concatenation with the + symbol使用 + 符号连接字符串
【发布时间】:2017-05-24 04:58:14
【问题描述】:

今天在看Antonio's Blog about toString() performance有一段话:

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

现在我很困惑,因为他说 今天 JVM 将 + 符号编译为字符串生成器(在大多数情况下),但我以前从未听说过或见过(代码)这样​​的东西.

有人可以举个例子JVM在哪里这样做以及在什么条件下发生

【问题讨论】:

  • @Eric 恐怕这与上述问题无关。因为在这个问题中,他明确指出 concat() 方法只接受 String 值,而 + 运算符会默默地将参数转换为 String(对对象使用 toString() 方法)。但是,我说的是发生在 StringBuilder 中的转换。如果缺少什么,请填写我。
  • @MehrajMalik 您是否看过已接受的答案?这几乎是一个骗局
  • @SomeJavaGuy 是的,我做到了。他提到 StringBuilder 转换发生在 + 操作符后面。但是,我的问题是这种情况是否一直发生,或者它需要一些特定的条件。如博客中所述(在大多数情况下会发生)。那么在哪些情况下它不转成StringBuilder呢?
  • 你可能想看看这个。 [pellegrino.link/2015/08/22/… 。用“+”添加字符串会给你 O(n^2) 复杂度,而 StringBuilder 的 append(String) 方法会给你 O(n) 复杂度

标签: java performance jvm tostring stringbuilder


【解决方案1】:

规则

“不要用 + 连接字符串!!!”

是错误的,因为它不完整,因此具有误导性。

规则是

不要在循环中用 + 连接字符串

并且这条规则仍然有效。最初的规则绝不是要在循环之外应用的!

一个简单的循环

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);

还有主张

今天,JVM 将 + 符号编译为字符串构建器(在大多数情况下)。

至少是误导性的,因为这种翻译已经用 Java 1.0 完成了(好吧,不是用 StringBuilder 而是用 StringBuffer,因为 StringBuilder 只是用 Java5 添加的)。


人们也可以争辩说这种说法

今天,JVM 将 + 符号编译为字符串构建器(在大多数情况下)。

完全是错误的,因为编译不是由 JVM 完成的。它由 Java 编译器完成。


问题是:Java 编译器什么时候使用StringBuilder.append(),什么时候使用其他机制?

Java 编译器(1.8 版)的源代码包含两个处理通过+ 运算符进行字符串连接的地方。

结论是,对于来自 OpenJDK 的 Java 编译器(即由 Oracle 分发的编译器),短语在大多数情况下表示始终。 (虽然这可能会随着 Java 9 而改变,或者可能是另一个 Java 编译器,比如 Eclipse 中包含的编译器使用了其他一些机制)。

【讨论】:

  • "因为编译不是由 JVM 完成的",哈哈,不错。但是,JLS 15.18.1 说“Java 编译器可以使用 StringBuffer 类”。但是,它没有说明当它不使用它时是什么情况。
  • @ChandlerBing 和 Thomas(关于 JVM 编译的令人印象深刻的发现),是的,完全正确。没有人提到它不使用 StringBuilder 进行连接的条件是什么。
  • 根据docs.oracle.com/javase/8/docs/api/java/lang/String.html"Java语言对字符串连接运算符(+)提供了特殊的支持,以及对其他对象到字符串的转换。字符串连接是通过StringBuilder(或StringBuffer)类实现的及其 append 方法。字符串转换通过 toString 方法实现,由 Object 定义并由 Java 中的所有类继承。有关字符串连接和转换的更多信息,请参阅 Gosling、Joy 和 Steele,Java 语言规范。"跨度>
  • @MehrajMalik 我目前只知道字符串连接的情况:两个操作数都是常量字符串(然后编译器用字符串文字替换它)或者至少一个操作数不是常量字符串(然后编译器使用 StringBuilder)。但我会尝试查看其他情况下的 java 编译器。
  • 由于非常量值的连接代码取决于特定的编译器,因此不可能说所有编译器都在这样做,因为这意味着我们声称知道所有编译器。我们可能会说,所有相关编译器,即javac和ecj,总是使用优化策略。顺便说一句,Java 9 的javac 不会使用StringBuilder,但是那是因为新策略被认为是更好的......
【解决方案2】:

Holger 的评论是正确的,在 java-9 中,+ 中的字符串连接将从 StringBuilder 变为 JRE 选择的策略,通过 invokedynamic。 jdk-9中String concatenation有6种可能的策略:

  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。实现的工作方式实际上非常疯狂,并且正在尝试进行高度优化。

所以,据我所知,那里没有任何建议是不好的。顺便说一句,这是 jdk 由 Aleksey Shipilev 投入的主要努力。他还在 jdk-9 中对 String 内部进行了重大更改,因为它们现在由 byte[] 支持,而不是 char[]。这是必需的,因为ISO_LATIN_1 字符串可以编码为单个字节(一个字符 - 一个字节),因此空间更少。

【讨论】:

    【解决方案3】:

    这种确切形式的陈述是错误的,它符合链接博客继续写废话的情况,就像你必须用Objects.toString(…)包装引用来处理null,例如"att1='" + Objects.toString(att1) + '\'' 而不仅仅是 "att1='" + att1 + '\''。没有必要这样做,显然,作者从未重新检查这些声明。

    JVM 不负责编译+ 运算符,因为该运算符只是一个源代码工件。它是编译器,例如javac 负责,虽然不能保证编译后的形式,但鼓励编译器使用Java Language Specification 的构建器:

    实现可以选择在一个步骤中执行转换和连接,以避免创建然后丢弃中间字符串对象。为了提高重复字符串连接的性能,Java 编译器可以使用 StringBuffer 类或类似技术来减少通过计算表达式创建的中间字符串对象的数量。

    请注意,即使编译器不执行此优化,在字节码级别上仍然没有+ 运算符之类的东西,因此编译器必须选择一个 JVM 可以理解的操作,例如使用String.concat,在您只是连接两个字符串的情况下,这可能比使用StringBuilder 更快。

    即使假设字符串连接的最差编译策略(仍然在规范内),说永远不要用+ 连接字符串是错误的,因为在定义编译时间常量时,使用+ 是唯一的选择,当然,编译时常量通常比在运行时使用 StringBuilder 更有效。

    实际上,应用于非常量字符串的 + 运算符在 Java 5 之前被编译为 StringBuffer 用法,在 Java 5 到 Java 8 中被编译为 StringBuilder 用法。当编译的代码与手册相同时分别使用StringBufferStringBuilder,不可能有性能差异。

    十多年前过渡到 Java 5 是第一次,通过 + 进行字符串连接明显胜过手动使用 StringBuffer,因为只需重新编译连接代码就可以使用可能更快StringBuilder 内部,而手动处理StringBuffer 的代码需要重写以使用该版本中引入的StringBuilder

    同样,Java 9 将使用 invokedynamic 指令编译字符串连接,从而允许 JRE 将其绑定到执行操作的实际代码,包括普通 Java 代码中无法实现的优化。所以只需要重新编译串串代码就可以得到这个特性,而没有等效的手动用法。

    也就是说,虽然前提是错误的,即字符串连接从未被认为是邪恶的,但建议是正确的,请不要犹豫。

    只有在少数情况下,您确实可以通过手动处理缓冲区来提高性能,即当您需要较大的初始容量或在循环中连接很多时并且该代码已被确定为实际的性能瓶颈通过分析工具

    【讨论】:

      【解决方案4】:

      当您使用 + 运算符连接字符串时,编译器会将连接代码转换为使用 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");
      

      【讨论】:

      • 这没有回答问题。 OP 询问编译器何时+ 转换为append
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-24
      • 2014-05-27
      • 2020-02-27
      • 1970-01-01
      • 2010-10-09
      相关资源
      最近更新 更多