StringBuilder与Java中toString()中的字符串连接

给定下面的2toString()实现,哪一个是首选的:

public String toString(){return "{a:"+ a + ", b:" + b + ", c: " + c +"}";}

public String toString(){StringBuilder sb = new StringBuilder(100);return sb.append("{a:").append(a).append(", b:").append(b).append(", c:").append(c).append("}").toString();}

?

更重要的是,考虑到我们只有3个属性,这可能不会有什么不同,但是在什么时候你会从+ Concat切换到StringBuilder

463238 次浏览

关键是你是在一个地方写一个连接,还是随着时间的推移积累它。

对于你给出的示例,显式使用StringBuilder是没有意义的。(查看第一个案例的编译代码。)

但是,如果您正在构建一个字符串,例如在循环中,请使用StringBuilder。

为了澄清,假设hugeArray包含数千个字符串,代码如下:

...String result = "";for (String s : hugeArray) {result = result + s;}

与以下相比,非常浪费时间和内存:

...StringBuilder sb = new StringBuilder();for (String s : hugeArray) {sb.append(s);}String result = sb.toString();

我更喜欢:

String.format( "{a: %s, b: %s, c: %s}", a, b, c );

…因为它简短易读。

我会没有优化这个速度,除非你在一个重复计数非常高的循环中使用它已经测量了性能差异。

我同意,如果你必须输出很多参数,这种形式可能会令人困惑(就像其中一条评论所说的)。在这种情况下,我会切换到更具可读性的形式(也许使用创建工具ToStringBuilder of apache-Commons-取自matt b的答案)并再次忽略性能。

版本1更可取,因为它更短并且编译器实际上会把它变成版本2-没有任何性能差异。

更重要的是,我们只有3个它可能不会使一个属性不同,但在什么时候你从Concat切换到Builder?

当您在循环中连接时-这通常是编译器无法自行替换StringBuilder的时候。

在大多数情况下,你不会看到这两种方法之间的实际差异,但很容易构建像这样的最坏情况:

public class Main{public static void main(String[] args){long now = System.currentTimeMillis();slow();System.out.println("slow elapsed " + (System.currentTimeMillis() - now) + " ms");
now = System.currentTimeMillis();fast();System.out.println("fast elapsed " + (System.currentTimeMillis() - now) + " ms");}
private static void fast(){StringBuilder s = new StringBuilder();for(int i=0;i<100000;i++)s.append("*");}
private static void slow(){String s = "";for(int i=0;i<100000;i++)s+="*";}}

输出是:

slow elapsed 11741 msfast elapsed 7 ms

问题是将+=追加到字符串会重建一个新字符串,因此它的成本与字符串的长度呈线性关系(两者之和)。

关于你的问题:

第二种方法会更快,但它的可读性较差,难以维护。就像我说的,在你的具体情况下,你可能看不到区别。

我可以指出,如果您要迭代集合并使用StringBuilder,您可能需要查看Apache Commons LangStringUtils.join()(不同的风格)?

无论性能如何,它都将为您节省创建StringBuilder和循环的时间。

Apache Commons-Lang有一个创建工具ToStringBuilder类,非常容易使用。它在处理追加逻辑和格式化您希望toString的外观方面做得很好。

public void toString() {ToStringBuilder tsb =  new ToStringBuilder(this);tsb.append("a", a);tsb.append("b", b)return tsb.toString();}

将返回看起来像com.blah.YourClass@abc1321f[a=whatever, b=foo]的输出。

或者以更简洁的形式使用链接:

public void toString() {return new ToStringBuilder(this).append("a", a).append("b", b").toString();}

或者,如果您想使用反射来包含类的每个字段:

public String toString() {return ToStringBuilder.reflectionToString(this);}

如果需要,您还可以自定义ToString的样式。

对于这样简单的弦,我更喜欢用

"string".concat("string").concat("string");

按顺序排列,我想说构造字符串的首选方法是使用StringBuilder、String#con at(),然后是重载的+运算符。当使用大字符串时,StringBuilder会显着提高性能,就像使用+运算符会显着降低性能(随着字符串大小的增加呈指数级下降)。使用. uncat()的一个问题是它会抛出NullPointerExceptions。

使toString方法尽可能具有可读性!

在我的书中,唯一的例外是,如果你能证明对我来说,它消耗了大量的资源:)(是的,这意味着分析)

另请注意,Java编译器生成的代码比Java早期版本中使用的手写“StringBuffer”方法更快。如果您使用“+”,此功能和未来的增强功能将免费提供。

我还和我的老板在使用append还是+的问题上发生了冲突。因为他们正在使用Append(我仍然无法弄清楚每次创建新对象时他们所说的)。所以我想做一些R&D。虽然我喜欢迈克尔·博格沃特的解释,但只是想展示一个解释,如果将来有人真的需要知道的话。

/**** @author Perilbrain*/public class Appc {public Appc() {String x = "no name";x += "I have Added a name" + "We May need few more names" + Appc.this;x.concat(x);// x+=x.toString(); --It creates new StringBuilder object before concatenation so avoid if possible//System.out.println(x);}
public void Sb() {StringBuilder sbb = new StringBuilder("no name");sbb.append("I have Added a name");sbb.append("We May need few more names");sbb.append(Appc.this);sbb.append(sbb.toString());// System.out.println(sbb.toString());}}

和上述类的拆卸出来作为

 .method public <init>()V //public Appc().limit stack 2.limit locals 2met001_begin:                                  ; DATA XREF: met001_slot000i.line 12aload_0 ; met001_slot000invokespecial java/lang/Object.<init>()V.line 13ldc "no name"astore_1 ; met001_slot001.line 14
met001_7:                                      ; DATA XREF: met001_slot001inew java/lang/StringBuilder //1st object of SBdupinvokespecial java/lang/StringBuilder.<init>()Vaload_1 ; met001_slot001invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\g/StringBuilder;ldc "I have Added a nameWe May need few more names"invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\g/StringBuilder;aload_0 ; met001_slot000invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\g/StringBuilder;invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;astore_1 ; met001_slot001.line 15aload_1 ; met001_slot001aload_1 ; met001_slot001invokevirtual java/lang/String.concat(Ljava/lang/String;)Ljava/lang/Strin\g;pop.line 18return //no more SB createdmet001_end:                                    ; DATA XREF: met001_slot000i ...
; ===========================================================================
;met001_slot000                                ; DATA XREF: <init>r ....var 0 is this LAppc; from met001_begin to met001_end;met001_slot001                                ; DATA XREF: <init>+6w ....var 1 is x Ljava/lang/String; from met001_7 to met001_end.end method;44-1=44; ---------------------------------------------------------------------------

; Segment type: Pure code.method public Sb()V //public void Sb.limit stack 3.limit locals 2met002_begin:                                  ; DATA XREF: met002_slot000i.line 21new java/lang/StringBuilderdupldc "no name"invokespecial java/lang/StringBuilder.<init>(Ljava/lang/String;)Vastore_1 ; met002_slot001.line 22
met002_10:                                     ; DATA XREF: met002_slot001iaload_1 ; met002_slot001ldc "I have Added a name"invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\g/StringBuilder;pop.line 23aload_1 ; met002_slot001ldc "We May need few more names"invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\g/StringBuilder;pop.line 24aload_1 ; met002_slot001aload_0 ; met002_slot000invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\g/StringBuilder;pop.line 25aload_1 ; met002_slot001aload_1 ; met002_slot001invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String;invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\g/StringBuilder;pop.line 28returnmet002_end:                                    ; DATA XREF: met002_slot000i ...

;met002_slot000                                ; DATA XREF: Sb+25r.var 0 is this LAppc; from met002_begin to met002_end;met002_slot001                                ; DATA XREF: Sb+9w ....var 1 is sbb Ljava/lang/StringBuilder; from met002_10 to met002_end.end method;96-49=48; ---------------------------------------------------------------------------

从上面的两个代码中,你可以看到Michael是对的。在每种情况下,只创建了一个SB对象。

从Java1.5开始,简单的一行连接“+”和StringBuilder.append()生成完全相同的字节码。

为了代码的易读性,请使用“+”。

2例外:

  • 多线程环境:StringBuffer
  • 循环中的连接:StringBuilder/StringBuffer

使用最新版本的Java(1.8)反汇编(javap -c)显示了编译器引入的优化。+sb.append()将生成非常相似的代码。然而,如果我们在for循环中使用+,检查行为将是值得的。

在for循环中使用+添加字符串

Java:

public String myCatPlus(String[] vals) {String result = "";for (String val : vals) {result = result + val;}return result;}

ByteCode:(for循环摘录)

12: iload         514: iload         416: if_icmpge     5119: aload_320: iload         522: aaload23: astore        625: new           #3                  // class java/lang/StringBuilder28: dup29: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V32: aload_233: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;36: aload         638: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;41: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;44: astore_245: iinc          5, 148: goto          12

使用stringbuilder.append添加字符串

Java:

public String myCatSb(String[] vals) {StringBuilder sb = new StringBuilder();for(String val : vals) {sb.append(val);}return sb.toString();}

ByteCdoe:(for循环摘录)

17: iload         519: iload         421: if_icmpge     4324: aload_325: iload         527: aaload28: astore        630: aload_231: aload         633: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;36: pop37: iinc          5, 140: goto          1743: aload_2

不过有一点明显的差异。在第一种情况下,使用+,为每个for循环迭代创建新的StringBuilder,并通过执行toString()调用(29到41)存储生成的结果。所以你在for循环中使用+运算符时生成了你真正不需要的中间字符串。

出于性能原因,不鼓励使用+=String连接)。原因是:JavaString是不可变的,每次完成新的连接时都会创建一个新的String(新的与旧的在字符串池具有不同的指纹)。创建新字符串会给GC带来压力并减慢程序:对象创建是昂贵的。

下面的代码应该同时使其更加实用和清晰。

public static void main(String[] args){// warming upfor(int i = 0; i < 100; i++)RandomStringUtils.randomAlphanumeric(1024);final StringBuilder appender = new StringBuilder();for(int i = 0; i < 100; i++)appender.append(RandomStringUtils.randomAlphanumeric(i));
// testingfor(int i = 1; i <= 10000; i*=10)test(i);}
public static void test(final int howMany){List<String> samples = new ArrayList<>(howMany);for(int i = 0; i < howMany; i++)samples.add(RandomStringUtils.randomAlphabetic(128));
final StringBuilder builder = new StringBuilder();long start = System.nanoTime();for(String sample: samples)builder.append(sample);builder.toString();long elapsed = System.nanoTime() - start;System.out.printf("builder - %d - elapsed: %dus\n", howMany, elapsed / 1000);
String accumulator = "";start = System.nanoTime();for(String sample: samples)accumulator += sample;elapsed = System.nanoTime() - start;System.out.printf("concatenation - %d - elapsed: %dus\n", howMany, elapsed / (int) 1e3);
start = System.nanoTime();String newOne = null;for(String sample: samples)newOne = new String(sample);elapsed = System.nanoTime() - start;System.out.printf("creation - %d - elapsed: %dus\n\n", howMany, elapsed / 1000);}

运行结果报告如下。

builder - 1 - elapsed: 132usconcatenation - 1 - elapsed: 4uscreation - 1 - elapsed: 5us
builder - 10 - elapsed: 9usconcatenation - 10 - elapsed: 26uscreation - 10 - elapsed: 5us
builder - 100 - elapsed: 77usconcatenation - 100 - elapsed: 1669uscreation - 100 - elapsed: 43us
builder - 1000 - elapsed: 511usconcatenation - 1000 - elapsed: 111504uscreation - 1000 - elapsed: 282us
builder - 10000 - elapsed: 3364usconcatenation - 10000 - elapsed: 5709793uscreation - 10000 - elapsed: 972us

不考虑1个连接的结果(JIT还没有完成它的工作),即使是10个连接,性能损失也是相关的;对于数千个连接,差异是巨大的。

从这个非常快速的实验中吸取的教训(很容易用上面的代码重现):永远不要使用+=将字符串连接在一起,即使在需要一些连接的非常基本的情况下(如上所述,创建新字符串无论如何都是昂贵的,并且会给GC带来压力)。

在Java9中,版本1应该更快,因为它被转换为invokedynamic调用。更多细节可以在JEP-280中找到:

这个想法是用一个简单的调用StringConcatFactory来替换整个StringBuilder追加舞蹈java.lang.invoke.,它将接受需要连接的值。

对于当前的编译器是否仍然需要使用StringBuilder似乎存在一些争论。所以我想我会给出我的2美分经验。

我有一个10k记录的JDBC结果集(是的,我需要所有这些记录在一个批处理中。)在我的机器上使用+运算符大约需要5分钟Java 1.8。使用stringBuilder.append("")对同一查询不到一秒钟。

所以区别是巨大的。在循环StringBuilder中要快得多。

我认为我们应该使用StringBuilder append方法。原因:

  1. String连接每次都会创建一个新的字符串对象(因为String是不可变对象),因此它将创建3个对象。

  2. 使用String Builder只会创建一个对象[StringBuilder是可变的],并将进一步的字符串附加到它。

这取决于字符串的大小。

请看下面的例子:

static final int MAX_ITERATIONS = 50000;static final int CALC_AVG_EVERY = 10000;
public static void main(String[] args) {printBytecodeVersion();printJavaVersion();case1();//str.concatcase2();//+=case3();//StringBuilder}
static void case1() {System.out.println("[str1.concat(str2)]");List<Long> savedTimes = new ArrayList();long startTimeAll = System.currentTimeMillis();String str = "";for (int i = 0; i < MAX_ITERATIONS; i++) {long startTime = System.currentTimeMillis();str = str.concat(UUID.randomUUID() + "---");saveTime(savedTimes, startTime);}System.out.println("Created string of length:" + str.length() + " in " + (System.currentTimeMillis() - startTimeAll) + " ms");}
static void case2() {System.out.println("[str1+=str2]");List<Long> savedTimes = new ArrayList();long startTimeAll = System.currentTimeMillis();String str = "";for (int i = 0; i < MAX_ITERATIONS; i++) {long startTime = System.currentTimeMillis();str += UUID.randomUUID() + "---";saveTime(savedTimes, startTime);}System.out.println("Created string of length:" + str.length() + " in " + (System.currentTimeMillis() - startTimeAll) + " ms");}
static void case3() {System.out.println("[str1.append(str2)]");List<Long> savedTimes = new ArrayList();long startTimeAll = System.currentTimeMillis();StringBuilder str = new StringBuilder("");for (int i = 0; i < MAX_ITERATIONS; i++) {long startTime = System.currentTimeMillis();str.append(UUID.randomUUID() + "---");saveTime(savedTimes, startTime);}System.out.println("Created string of length:" + str.length() + " in " + (System.currentTimeMillis() - startTimeAll) + " ms");
}
static void saveTime(List<Long> executionTimes, long startTime) {executionTimes.add(System.currentTimeMillis() - startTime);if (executionTimes.size() % CALC_AVG_EVERY == 0) {out.println("average time for " + executionTimes.size() + " concatenations: "+ NumberFormat.getInstance().format(executionTimes.stream().mapToLong(Long::longValue).average().orElseGet(() -> 0))+ " ms avg");executionTimes.clear();}}

输出:

java字节码版本:8
java.version:1.8.0_144
[str1.concat(str2)]
10000个连接的平均时间:0.096 ms avg
10000个连接的平均时间:0.185 ms avg
10000个连接的平均时间:0.327 ms avg
10000个连接的平均时间:0.501 ms avg
10000个连接的平均时间:0.656 ms avg
创建长度为1950000的字符串17745 ms
[str1+=str2]
10000个连接的平均时间:0.21 ms avg
10000个连接的平均时间:0.652 ms avg
10000个连接的平均时间:平均1.129 ms
10000个连接的平均时间:1.727 ms avg
10000个连接的平均时间:2.302 ms avg
创建长度为1950000的字符串60279 ms
[str1.append(str2)]
10000个连接的平均时间:0.002 ms avg
10000个连接的平均时间:0.002 ms avg
10000个连接的平均时间:0.002 ms avg
10000个连接的平均时间:0.002 ms avg
10000个连接的平均时间:0.002 ms avg
创建长度为1950000的字符串100 ms

随着字符串长度的增加,#0和#1的连接时间也会增加,后者更有效,但仍然是非常数
这就是StringBuilder肯定需要的地方。

附言:我不认为何时在Java中使用StringBuilder真的是这样的重复。
这个问题讨论的是toString(),大多数时候它不执行大字符串的连接。


2019更新

java8次开始,事情发生了一些变化。似乎现在(java13),+=的连接时间实际上与str.concat()相同。然而#3连接时间保持不变。(上面的原始帖子略有编辑,以添加更详细的输出)

java字节码版本:13
java.version:13.0.1
[str1.concat(str2)]
10000个连接的平均时间:0.047 ms avg
10000个连接的平均时间:0.1 ms avg
10000个连接的平均时间:0.17 ms avg
10000个连接的平均时间:0.255 ms avg
10000个连接的平均时间:0.336 ms avg
创建长度为1950000的字符串9147 ms
[str1+=str2]
10000个连接的平均时间:0.037 ms avg
10000个连接的平均时间:0.097 ms avg
10000个连接的平均时间:0.249 ms avg
10000个连接的平均时间:0.298 ms avg
10000个连接的平均时间:0.326 ms avg
创建长度为1950000的字符串10191 ms
[str1.append(str2)]
10000个连接的平均时间:0.001 ms avg
10000个连接的平均时间:0.001 ms avg
10000个连接的平均时间:0.001 ms avg
10000个连接的平均时间:0.001 ms avg
10000个连接的平均时间:0.001 ms avg
创建长度为1950000的字符串43 ms

值得注意的是,与bytecode:8/java.version:8相比,bytecode:8/java.version:13组合具有良好的性能优势

使用'+'的性能明智的字符串连接成本更高,因为它必须制作一个全新的字符串副本,因为字符串在java中是不可变的。如果连接非常频繁,例如:在循环中,这起着特殊的作用。以下是我的想法建议当我尝试做这样的事情:

在此处输入图片描述

一般规则:

  • 在单个字符串赋值中,使用String连接是可以的。
  • 如果您要循环构建一大块字符数据,请使用StringBuffer。
  • 在String上使用+=总是比使用StringBuffer效率低,所以它应该敲响警钟-但在某些情况下,与易读性问题相比,获得的优化可以忽略不计,所以使用你的常识。

这里有一个围绕这个主题的不错的Jon Skeet博客

这是我在Java8中检查的内容

  • 使用字符串连接
  • 使用StringBuilder

    long time1 = System.currentTimeMillis();usingStringConcatenation(100000);System.out.println("usingStringConcatenation " + (System.currentTimeMillis() - time1) + " ms");
    time1 = System.currentTimeMillis();usingStringBuilder(100000);System.out.println("usingStringBuilder " + (System.currentTimeMillis() - time1) + " ms");
    
    private static void usingStringBuilder(int n){StringBuilder str = new StringBuilder();for(int i=0;i<n;i++)str.append("myBigString");}
    private static void usingStringConcatenation(int n){String str = "";for(int i=0;i<n;i++)str+="myBigString";}

It's really a nightmare if you are using string concatenation for large number of strings.

usingStringConcatenation 29321 msusingStringBuilder 2 ms

我认为这张图片对于比较使用String的所有类非常有用:

输入图片描述

值得一提的是,作为由@ZhekaKozlov指出

+比Java9更快,除非JVM不知道如何优化它(例如在循环中连接)。

我检查了以下代码的字节码(Java17):

public class StringBM {public String toStringPlus(String a) {return "{a:" + a + ", b:" + ", c: " + "}";}
public String toStringBuilder(String a) {StringBuilder sb = new StringBuilder(100);return sb.append("{a:").append(a).append(", b:").append(", c:").append("}").toString();}}

对于toStringPlus

 0: aload_11: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;6: areturn

对于toStringBuilder

 0: new           #11                 // class java/lang/StringBuilder3: dup4: bipush        1006: invokespecial #13                 // Method java/lang/StringBuilder."<init>":(I)V9: astore_210: aload_211: ldc           #16                 // String {a:13: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;16: aload_117: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;20: ldc           #22                 // String , b:22: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;25: ldc           #24                 // String , c:27: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;30: ldc           #26                 // String }32: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;35: invokevirtual #28                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;38: areturn

+版本简单地调用动态函数makeConcatWithConstants并传入方法参数{a:, b:, c:}(\u0001是参数占位符)。
StringBuilder版本必须以“诚实”的方式完成。
我想我们现在可以看到为什么+更快了。