赋值时的字符串串联是否有效?

Is String concatenation on assignment efficient?

我知道使用“+”连接运算符构建字符串效率很低,这就是为什么推荐使用 StringBuilder class,但我想知道这种模式是否效率低下也是吗?

String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;

我猜编译器会不会优化赋值?

此特定示例将由编译器内联:

String a = "a";
String b = "bb";
String c = "ccc";
String some = a + "\t" + b + "\t" + c;

Java 9+ 将使用 invokedynamic with makeConcatWithConstants 内联它以提高效率。根据 javap -v 输出:

Code:
  stack=3, locals=5, args_size=1
     0: ldc           #2                  // String a
     2: astore_1
     3: ldc           #3                  // String bb
     5: astore_2
     6: ldc           #4                  // String ccc
     8: astore_3
     9: aload_1
    10: aload_2
    11: aload_3
    12: invokedynamic #5,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    17: astore        4
    19: return

但是如果abc是编译时常量,编译器会进一步优化代码:

final String a = "a";
final String b = "bb";
final String c = "ccc";
String some = a + "\t" + b + "\t" + c;

some将加载一个常量值:

Code:
  stack=1, locals=5, args_size=1
     0: ldc           #2                  // String a
     2: astore_1
     3: ldc           #3                  // String bb
     5: astore_2
     6: ldc           #4                  // String ccc
     8: astore_3
     9: ldc           #5                  // String a\tbb\tccc
    11: astore        4
    13: return

在其他情况下,例如for 循环编译器可能无法生成优化代码,因此 StringBuilder 可能更快。

在一般情况下,使用 + 和使用 StringBuilder 的字符串连接是绝对正确且有效的。但是在不同的情况下,与 + 的连接效率低于使用 StringBuilder.

不在循环中的字符串连接 - 高效!!!

这会产生很好的性能,因为 JVM 使用 StringBuilder.

转换它
String some = a + "\t" + b + "\t" + c + "\t" + d + "\t" + e;

这没问题,因为 JVM 在内部将此代码更改为以下代码:

String some = new StringBuilder().append(a).append('\t').append(c).append('\t')
                                 .append(d).append('\t').append(e).toString();

P.S. StringBuilder 有内部缓冲区 char[]。如果您知道结果字符串有多长,那么最好在开始时保留整个缓冲区。例如。如果最终字符串将 最多 1024 个字符 ,那么你可以 new StringBuilder(1024)

循环中的字符串连接 - 效率不高!!!

这会导致性能变差,因为 JVM 无法用一个 StringBuilder 包裹 while 循环,如下所示:

StringBuilder buf = new StringBuilder();

for (int i = 0; i < 10; i++)
    buf.append(a).append('\t').append(c).append('\t')
       .append(d).append('\t').append(e).append('t');

String some = buf.toString();

但 JVM 仍然能够优化每个循环迭代中的所有连接;像这样:

String some = "";

for (int i = 0; i < 10; i++) {
    some = new StringBuilder(some).append(a).append('\t').append(c).append('\t')
                               .append(d).append('\t').append(e).append('t');
}

如您所见,在循环中使用字符串连接有一些缺点。

您的前提“使用“+”连接运算符构建字符串的效率非常低”是不正确的。首先,字符串连接本身并不是一个廉价的操作,因为它意味着创建一个包含所有连接字符串的新字符串,因此需要复制字符内容。但这始终适用,无论您如何做到这一点。

当你使用 + 运算符时,你是在告诉你想做什么,而不是说如何去做。甚至 Java 语言规范也不需要特定的实现策略,除了编译时常量的连接必须在编译时完成。因此对于编译时常量,+ 运算符 最有效的解决方案¹。

实际上,从 Java 5 到 Java 8 的所有常用编译器都使用 StringBuilder 底层生成代码(在 Java 5 之前,它们使用 StringBuffer).这适用于像您这样的语句,因此将其替换为手动 StringBuilder 使用不会有太大好处。通过提供合理的初始容量,您可能会比典型的编译器生成的代码稍微好一些,但仅此而已。

从 Java9 开始,编译器生成一条 invokedynamic 指令,允许运行时提供执行串联的实际代码。这可能是一个 StringBuilder 代码,类似于过去使用的代码,但也完全不同。最值得注意的是,运行时提供的代码可以访问应用程序代码无法访问的实现特定功能。所以现在,通过 + 的字符串连接甚至可以比基于 StringBuilder 的代码更快。

由于这仅适用于单个连接表达式,因此在使用多个语句甚至循环执行字符串构造时,在整个构造过程中始终如一地使用 StringBuilder 可能比多个连接操作更快。但是,由于代码在优化环境中运行,JVM 可以识别其中的一些模式,因此甚至不能确定。

现在是记住旧规则的时候了,只有在性能出现实际问题时才尝试优化性能。并始终使用公正的测量工具来验证尝试的优化是否真正提高了性能。关于性能优化技巧,有很多流传甚广的神话,无论是错误的还是过时的。

¹ 除非您有重复的部分并希望减小 class 文件的大小