StringBuilder/StringBuffer与"+"运算符的区别

46
我正在阅读 Bruce Tate 和 Justin Gehtland 的 "Better, Faster, Lighter Java",并熟悉敏捷型团队中可读性要求,例如 Robert Martin 在他的清晰编码书籍中所讨论的内容。在我现在所在的团队中,我被明确告知不要使用 + 运算符,因为它会在运行时创建额外(且不必要)的字符串对象。
但是这篇文章,写于 2004 年,谈到对象分配大约需要 10 条机器指令(基本上是免费的)。
它还谈到了 GC 如何在这种环境下降低成本。
在使用 +StringBuilderStringBuffer 中的实际性能权衡是什么?(在我的情况下只能使用 StringBuffer,因为我们受到 Java 1.4.2 的限制。)
对我来说,StringBuffer 导致了丑陋、难以阅读的代码,就像 Tate 的书中的一些例子所示。而 StringBuffer 是线程同步的,这似乎有其自身的成本,超过了使用 + 运算符的 "危险性"。
你对此有何看法/意见?

2
类似的问题在这里:https://dev59.com/3W445IYBdhLWcg3w5OII - Navi
可能是Java中toString()中的StringBuilder vs String concatenation的重复问题。 - Joshua Goldberg
4个回答

60

使用字符串连接操作时,编译器会将其转换为StringBuilder操作。

为了查看编译器的操作,我将使用一个示例类进行编译,并使用jad反编译,以查看生成的字节码。

原始类:

public void method1() {
    System.out.println("The answer is: " + 42);
}

public void method2(int value) {
    System.out.println("The answer is: " + value);
}

public void method3(int value) {
    String a = "The answer is: " + value;
    System.out.println(a + " what is the question ?");
}

反编译后的类:

public void method1()
{
    System.out.println("The answer is: 42");
}

public void method2(int value)
{
    System.out.println((new StringBuilder("The answer is: ")).append(value).toString());
}

public void method3(int value)
{
    String a = (new StringBuilder("The answer is: ")).append(value).toString();
    System.out.println((new StringBuilder(String.valueOf(a))).append(" what is the question ?").toString());
}
  • method1中,编译器在编译时执行了操作。
  • method2中,String串联相当于手动使用StringBuilder
  • method3中,String串联绝对是不好的,因为编译器创建了第二个StringBuilder而不是重用先前的。

所以我的简单规则是,除非需要再次串联结果(例如在循环中或需要存储中间结果时),否则串联是好的。


3
在Java语言规范中,它建议编译器会这样做,但也存在不这样做的可能性。15.18.1.2字符串连接的优化。 - avgvstvs
你知道SUN JVM中这个优化已经生效多久了吗?如果IBM JVM中也存在的话,它是否也被应用了呢? - avgvstvs
我认为这个功能可能在Sun的JVM 1.4版本及以上存在,但我不知道它是否存在于IBM的JVM中。您可以使用诸如jad之类的反编译工具来确认。 - gabuzo
注意:在具有较小字符串的Android设备(例如,在512MB设备上为1MB)中,方法2运行没有问题,但方法3会导致OutOfMemory异常。这是因为第二个StringBuilder吗? - Michael

24

您的团队需要了解 避免重复字符串连接的原因

当您在循环中创建字符串时,特别是如果您不确定循环中会有几次迭代,使用 StringBuffer 是有意义的时候确实存在。请注意,这不仅涉及到创建新对象的问题,还涉及到复制已经添加的所有文本数据的问题。同时,请记住,如果不考虑垃圾回收,对象分配只是“基本上免费”的。是的,如果当前代有足够的空间,那基本上只是增加一个指针……但:

  • 该内存必须在某个时刻被清除。这不是免费的。
  • 您缩短了下一次 GC 所需的时间。GC 不是免费的。
  • 如果您的对象存活到下一代,它可能需要更长的时间才能被清理 - 再次,不是免费的。

所有这些事情都是 相对便宜的,因为它通常不值得追求优雅的设计而避免创建对象......但您不应将它们视为免费

另一方面,在您不需要中间字符串的情况下,使用 StringBuffer 没有任何意义。例如:

String x = a + b + c + d;

至少和...一样高效:

StringBuffer buffer = new StringBuffer();
buffer.append(a);
buffer.append(b);
buffer.append(c);
buffer.append(d);
String x = buffer.toString();

1
编译器将两个字符串 "Subject " + " Predicate" 转换为... - avgvstvs
buf.append("主语").append("谓语").toString() - avgvstvs
1
@matt.seil:实际上,如果涉及到字符串常量,Java编译器将只使用“主语谓语”。但是,否则它在幕后使用StringBuffer/StringBuilder。 - Jon Skeet
好的:如果使用常量,就不会进行任何调用,因此从技术上讲,根本没有性能损失?编译器只是将A + B连接成一个单独的字符串对象AB? - avgvstvs
1
@matt.seil:确实 - 在这种情况下使用StringBuffer会影响性能,创建比所需更多的对象... - Jon Skeet
当然,在多线程环境中您只需要使用StringBuffer。如果您不需要同步,可以使用StringBuilder。 - Felix S

6

对于小的字符串连接,您可以简单地使用String和+来提高可读性。性能不会受到影响。但是,如果您正在执行大量的连接操作,请使用StringBuffer。


0
其他答案已经提到,当您在循环中创建字符串时应该使用StringBuilder。然而,大多数循环都是针对集合的,并且从Java 8开始,可以使用Collectors类中的joining方法将集合转换为字符串。
例如,在下面的代码中:
String result = Arrays.asList("Apple", "Banana", "Orange").stream() 
                     .collect(Collectors.joining(", ", "<", ">")); 

result 将会是:<苹果, 香蕉, 橙子>


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