在Java中toString()中的StringBuilder vsstring连接

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

 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

版本1是更可取的,因为它更短, 编译器实际上将它变成版本2 – 没有任何性能差异。

更重要的是,由于我们只有3个属性,它可能没有什么区别,但是在什么时候从concat切换到builder?

在循环连接处 – 通常是编译器StringBuilderreplaceStringBuilder

关键在于你是在一个地方编写一个单独的连接还是一段时间内累加它。

对于你给的例子,明确使用StringBuilder没有意义。 (查看你的第一个案例的编译代码。)

但是,如果你正在build立一个string,例如在一个循环内,使用StringBuilder。

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

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

…因为它简短易读

不会优化这个速度,除非你在一个非常高的重复次数的循环内使用它并测量了性能差异。

我同意,如果你必须输出很多的参数,这个表单可能会让人困惑(就像其中一个评论所说的)。 在这种情况下,我会切换到一个更可读的forms(也许使用apache-commons的toStringBuilder – 从马特b的答案),并再次忽略性能。

在大多数情况下,你不会看到两种方法之间的实际差异,但是很容易构build这样一个最糟糕的情况:

 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 ms fast elapsed 7 ms 

问题是+ =追加到一个string重build一个新的string,所以它花费线性的长度(两者之和)的string的长度。

所以 – 对你的问题:

第二种方法会更快,但它的可读性和维护难度较低。 正如我所说,在你的具体情况下,你可能看不出有什么不同。

我也和我的老板发生过冲突,究竟是使用append还是+。因为他们使用的是Append(每当创build一个新的对象的时候,我仍然无法弄清楚)。 所以我想做一些R&D。虽然我爱Michael Borgwardt的解释,但是只是想解释一下,如果有人真的需要知道将来。

 /** * * @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 2 met001_begin: ; DATA XREF: met001_slot000i .line 12 aload_0 ; met001_slot000 invokespecial java/lang/Object.<init>()V .line 13 ldc "no name" astore_1 ; met001_slot001 .line 14 met001_7: ; DATA XREF: met001_slot001i new java/lang/StringBuilder //1st object of SB dup invokespecial java/lang/StringBuilder.<init>()V aload_1 ; met001_slot001 invokevirtual 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_slot000 invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\ g/StringBuilder; invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String; astore_1 ; met001_slot001 .line 15 aload_1 ; met001_slot001 aload_1 ; met001_slot001 invokevirtual java/lang/String.concat(Ljava/lang/String;)Ljava/lang/Strin\ g; pop .line 18 return //no more SB created met001_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 2 met002_begin: ; DATA XREF: met002_slot000i .line 21 new java/lang/StringBuilder dup ldc "no name" invokespecial java/lang/StringBuilder.<init>(Ljava/lang/String;)V astore_1 ; met002_slot001 .line 22 met002_10: ; DATA XREF: met002_slot001i aload_1 ; met002_slot001 ldc "I have Added a name" invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\ g/StringBuilder; pop .line 23 aload_1 ; met002_slot001 ldc "We May need few more names" invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\ g/StringBuilder; pop .line 24 aload_1 ; met002_slot001 aload_0 ; met002_slot000 invokevirtual java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lan\ g/StringBuilder; pop .line 25 aload_1 ; met002_slot001 aload_1 ; met002_slot001 invokevirtual java/lang/StringBuilder.toString()Ljava/lang/String; invokevirtual java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lan\ g/StringBuilder; pop .line 28 return met002_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 ; --------------------------------------------------------------------------- 

从上面的两个代码你可以看到迈克尔是正确的。在每种情况下,只有一个SB对象被创build。

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

所以为了代码可读性,使用“+”。

2例外:

  • multithreading环境:StringBuffer
  • 循环中的连接:StringBuilder / StringBuffer

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

在for循环中使用+添加string

Java的:

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

ByteCode 🙁 for循环摘录)

 12: iload 5 14: iload 4 16: if_icmpge 51 19: aload_3 20: iload 5 22: aaload 23: astore 6 25: new #3 // class java/lang/StringBuilder 28: dup 29: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V 32: aload_2 33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: aload 6 38: 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_2 45: iinc 5, 1 48: goto 12 

使用stringbuilder.append添加string

Java的:

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

ByteCdoe 🙁 for循环摘录)

 17: iload 5 19: iload 4 21: if_icmpge 43 24: aload_3 25: iload 5 27: aaload 28: astore 6 30: aload_2 31: aload 6 33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 36: pop 37: iinc 5, 1 40: goto 17 43: aload_2 

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

Apache Commons-Lang有一个ToStringBuilder类,它非常易于使用。 它在处理append逻辑和你希望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]

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

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

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

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

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

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

这个想法是用一个简单的invokedynamic调用来replace整个StringBuilder的append dance,调用java.lang.invoke.StringConcatFactory,它将接受需要连接的值。

出于性能原因,不鼓励使用+=String concatenation)。 原因是:Java String是不可变的,每当新的串联完成时,一个新的String被创build(新的String与已经在string池中的旧string具有不同的指纹)。 创build新的string会给GC带来压力,并会降低程序的运行速度:对象的创build非常昂贵。

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

 public static void main(String[] args) { // warming up for(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)); // testing for(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: 132us concatenation - 1 - elapsed: 4us creation - 1 - elapsed: 5us builder - 10 - elapsed: 9us concatenation - 10 - elapsed: 26us creation - 10 - elapsed: 5us builder - 100 - elapsed: 77us concatenation - 100 - elapsed: 1669us creation - 100 - elapsed: 43us builder - 1000 - elapsed: 511us concatenation - 1000 - elapsed: 111504us creation - 1000 - elapsed: 282us builder - 10000 - elapsed: 3364us concatenation - 10000 - elapsed: 5709793us creation - 10000 - elapsed: 972us 

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

从这个非常快速的实验中学到的教训(很容易用上面的代码重现):即使在需要几个连接的非常基本的情况下,也不要使用+=连接string(正如所说的那样,创build新string无论如何都是昂贵的,在GC上)。

使toString方法尽可能可读!

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

另请注意,Java 5编译器生成的代码比早期版本的Java中使用的手写“StringBuffer”方法更快。 如果你使用“+”这个,未来的增强function是免费的。

我可以指出,如果你要迭代一个集合并使用StringBuilder,你可能想看看Apache Commons Lang和StringUtils.join() (以不同的风格)?

无论性能如何,这将节省您创buildStringBuilders和循环的百万次。

我比较了四种不同的方法来比较性能。 我完全不知道gc会发生什么,但对我来说重要的是时间。 编译器是重要的因素。我在window8.1平台下使用了jdk1.8.0_45。

 concatWithPlusOperator = 8 concatWithBuilder = 130 concatWithConcat = 127 concatStringFormat = 3737 concatWithBuilder2 = 46 

 public class StringConcatenationBenchmark { private static final int MAX_LOOP_COUNT = 1000000; public static void main(String[] args) { int loopCount = 0; long t1 = System.currentTimeMillis(); while (loopCount < MAX_LOOP_COUNT) { concatWithPlusOperator(); loopCount++; } long t2 = System.currentTimeMillis(); System.out.println("concatWithPlusOperator = " + (t2 - t1)); long t3 = System.currentTimeMillis(); loopCount = 0; while (loopCount < MAX_LOOP_COUNT) { concatWithBuilder(); loopCount++; } long t4 = System.currentTimeMillis(); System.out.println("concatWithBuilder = " + (t4 - t3)); long t5 = System.currentTimeMillis(); loopCount = 0; while (loopCount < MAX_LOOP_COUNT) { concatWithConcat(); loopCount++; } long t6 = System.currentTimeMillis(); System.out.println("concatWithConcat = " + (t6 - t5)); long t7 = System.currentTimeMillis(); loopCount = 0; while (loopCount < MAX_LOOP_COUNT) { concatStringFormat(); loopCount++; } long t8 = System.currentTimeMillis(); System.out.println("concatStringFormat = " + (t8 - t7)); long t9 = System.currentTimeMillis(); loopCount = 0; while (loopCount < MAX_LOOP_COUNT) { concatWithBuilder2(); loopCount++; } long t10 = System.currentTimeMillis(); System.out.println("concatWithBuilder2 = " + (t10 - t9)); } private static void concatStringFormat() { String s = String.format("%s %s %s %s ", "String", "String", "String", "String"); } private static void concatWithConcat() { String s = "String".concat("String").concat("String").concat("String"); } private static void concatWithBuilder() { StringBuilder builder=new StringBuilder("String"); builder.append("String").append("String").append("String"); String s = builder.toString(); } private static void concatWithBuilder2() { String s = new StringBuilder("String").append("String").append("String").append("String").toString(); } private static void concatWithPlusOperator() { String s = "String" + "String" + "String" + "String"; } } 

目前的编译器是否仍然需要使用StringBuilder,似乎有一些争议。 所以我想我会给我2美分的经验。

我有一个10klogging的JDBC结果集(是的,我需要在一个批处理中使用它们。)使用+运算符在我的计算机上使用Java 1.8需要大约5分钟的时间。 对于同一个查询,使用stringBuilder.append("")不到一秒钟。

所以差异是巨大的。 在一个循环内部StringBuilder要快得多。

看下面的例子:

 //java8 static void main(String[] args) { case1(); case2(); case3(); } static void case1() { 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() { 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() { 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(); } } 

输出:

平均时间为10000个连接:0.096 ms avg
平均时间为10000个连接:0.185 ms avg
平均时间为10000个连接:平均0.327毫秒
10000个连接的平均时间:0.501 ms avg
10000个连接的平均时间:0.656 ms avg
创build的string长度:1950000在17745毫秒
10000个连接的平均时间:平均0.21毫秒
10000个连接的平均时间:平均0.652毫秒
平均时间为10000个连接:1.129 ms avg
平均时间为10000个连接:平均1.727毫秒
平均时间为10000个连接:2.302 ms平均
创build的string长度:1950000在60279毫秒
平均时间为10000个连接:0.002毫秒平均
平均时间为10000个连接:0.002毫秒平均
平均时间为10000个连接:0.002毫秒平均
平均时间为10000个连接:0.002毫秒平均
平均时间为10000个连接:0.002毫秒平均
创build的string长度:1950000在100毫秒

随着string长度的增加,连接时间也增加。
那是StringBuilder肯定需要的地方。
正如你看到的,串联: UUID.randomUUID()+"---"并不会真正影响时间。

PS:我不认为什么时候在Java中使用StringBuilder实际上是这个的重复。
这个问题谈到了toString() ,大多数情况下不会执行巨大string的连接。

对于我喜欢使用的简单string

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

为了说明,构buildstring的首选方法是使用StringBuilder,String#concat(),然后重载+运算符。 在使用大string时,StringBuilder会显着提高性能,就像使用+运算符一样,会大大降低性能(随着string大小的增加呈指数级下降)。 使用.concat()的一个问题是它可以抛出NullPointerExceptions。

我想我们应该用StringBuilder追加方法去。 原因是

  1. string连接会每次创build一个新的string对象(因为string是不可变的对象),所以它会创build3个对象。

  2. 使用String构build器只会创build一个对象[StringBuilder是可变的],而后面的string会被附加到它。