StringBuilder vs String concatenation in toString() in Java
Implementations below,which one is preferred:
1 2 3 |
黄金
ZZU1
?
更重要的是,给我们的只有三种性能,这可能不是一种不同,但你从
版本1更可取,因为它较短,编译器实际上会将其转换为版本2—没有任何性能差异。
More importantly given we have only 3
properties it might not make a
difference, but at what point do you
switch from concat to builder?
在您要连接到一个循环中的时候——这通常是编译器不能自己替换
关键是您是在一个地方编写一个单一的串联,还是随着时间的推移累积它。
对于您给出的示例,显式使用StringBuilder没有意义。(查看第一个案例的编译代码。)
但是,如果您正在构建一个字符串(例如在循环中),请使用StringBuilder。
要澄清,假设hugearray包含数千个字符串,请执行以下代码:
与以下情况相比,时间和记忆非常浪费:
1 2 3 4 5 6 |
我更喜欢:
1 |
…因为它简短易读。
我不会在速度上对此进行优化,除非您在具有非常高重复计数的循环中使用它并测量了性能差异。
我同意,如果必须输出大量参数,那么这个表单可能会变得混乱(就像其中一条评论所说)。在本例中,我将切换到更可读的形式(可能使用ApacheCommons的ToStringBuilder——取自MattB的答案),然后再次忽略性能。
在大多数情况下,您不会看到这两种方法之间的实际差异,但是很容易构建一个最坏情况的场景,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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+="*"; } } |
输出是:
1 2 | slow elapsed 11741 ms fast elapsed 7 ms |
问题是,to+=append to字符串会重建一个新的字符串,因此它的开销与字符串的长度成线性关系(两者之和)。
所以-关于你的问题:
第二种方法会更快,但可读性更低,维护起来更困难。正如我所说,在您的具体案例中,您可能看不到区别。
我还和我的老板在是否使用append或+这个问题上发生了冲突。因为他们使用append(我仍然不能像他们说的那样,每次创建一个新对象时)。所以我想做一些研发工作。虽然我喜欢迈克尔·博格华德的解释,但我只是想解释一下,如果将来有人真的需要知道的话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /** * * @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()); } } |
上述类的分解结果为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | .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 ; --------------------------------------------------------------------------- |
从上面的两个代码中,您可以看到Michael是对的。在每种情况下,只创建一个SB对象。
自Java 1.5以来,与"+"和StrugBuudio.AppEnter()的简单单行连接生成完全相同的字节码。
因此,为了代码的可读性,使用"+"。
2例外情况:
- 多线程环境:StringBuffer
- 循环中的串联:StringBuilder/StringBuffer
使用Java(1.8)的最新版本,反汇编(EDCOX1×0)表示编译器所介绍的优化。
在for循环中使用+添加字符串
爪哇:
1 2 3 4 5 6 7 |
字节码:(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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添加字符串
爪哇:
1 2 3 4 5 6 7 |
BYTECDOE:(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 |
不过有一点明显的区别。在第一种情况下,使用
在Java 9中,版本1应该更快,因为它被转换为EDCOX1 16调用。更多详情请参见jep-280:
The idea is to replace the entire StringBuilder append dance with a simple invokedynamic call to java.lang.invoke.StringConcatFactory, that will accept the values in the need of concatenation.
出于性能原因,不鼓励使用
下面的代码应该使它更实际和清晰的同时。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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 ", 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 ", 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 ", howMany, elapsed / 1000); } |
运行结果报告如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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个连接,性能惩罚也是相关的;对于数千个连接,差异是巨大的。
从这个非常快速的实验中获得的经验教训(很容易用上面的代码重现):即使在需要一些连接的非常基本的情况下,也不要使用
ApacheCommonsLang有一个非常容易使用的ToStringBuilder类。它可以很好地处理附加逻辑,也可以格式化您希望ToString的外观。
1 2 3 4 5 6 | public void toString() { ToStringBuilder tsb = new ToStringBuilder(this); tsb.append("a", a); tsb.append("b", b) return tsb.toString(); } |
将返回类似于
或者以更精简的形式使用链接:
1 2 3 | public void toString() { return new ToStringBuilder(this).append("a", a).append("b", b").toString(); } |
或者,如果要使用反射来包含类的每个字段:
1 2 3 |
如果需要,还可以自定义ToString的样式。
尽可能使ToString方法可读!
在我的书中唯一的例外是,如果你能向我证明它消耗了大量的资源:)(是的,这意味着分析)
还要注意的是,Java 5编译器生成的代码比在早期版本的Java中使用的手写"StringBuffer"方法更快。如果您使用"+"这个和未来的增强是免费的。
对于当前的编译器是否仍然需要使用StringBuilder,似乎存在一些争论。所以我想我会给我2美分的经验。
我有一个由10万条记录组成的
所以差别很大。在一个循环中,
使用"+"的性能明智的字符串连接是昂贵的,因为它必须生成一个新的字符串副本,因为字符串在Java中是不可变的。如果连接非常频繁,例如:在循环中,这将起到特殊的作用。当我尝试做这样的事情时,我的想法建议如下:
一般规则:
- 在单个字符串分配中,可以使用字符串串联。
- 如果要循环生成一大块字符数据,请使用StringBuffer。
- 在字符串上使用+=总是比使用StringBuffer效率低,因此它应该敲响警钟-但在某些情况下,与可读性问题相比,所获得的优化是微不足道的,因此请使用您的常识。
这里有一个关于这个主题的很好的乔恩·斯基特博客。
见下例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | //java8 static void main(String[] args) { case1();//str.concat case2();//+= case3();//StringBuilder } 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(); } } |
输出:
average time for 10000 concatenations: 0.096 ms avg
average time for 10000 concatenations: 0.185 ms avg
average time for 10000 concatenations: 0.327 ms avg
average time for 10000 concatenations: 0.501 ms avg
average time for 10000 concatenations: 0.656 ms avg
Created string of length:1950000 in 17745 ms
average time for 10000 concatenations: 0.21 ms avg
average time for 10000 concatenations: 0.652 ms avg
average time for 10000 concatenations: 1.129 ms avg
average time for 10000 concatenations: 1.727 ms avg
average time for 10000 concatenations: 2.302 ms avg
Created string of length:1950000 in 60279 ms
average time for 10000 concatenations: 0.002 ms avg
average time for 10000 concatenations: 0.002 ms avg
average time for 10000 concatenations: 0.002 ms avg
average time for 10000 concatenations: 0.002 ms avg
average time for 10000 concatenations: 0.002 ms avg
Created string of length:1950000 in 100 ms
随着字符串长度的增加,连接时间也会增加。这正是
P.S.:我不认为何时在Java中使用StrugBu建器实际上是一个复制品。这个问题讨论的是
我比较了四种不同的方法来比较性能。我完全不知道GC会发生什么,但对我来说最重要的是时间。编译器是这里的重要因素,我在Window8.1平台下使用了JDK1.8.0 U45。
1 2 3 4 5 | concatWithPlusOperator = 8 concatWithBuilder = 130 concatWithConcat = 127 concatStringFormat = 3737 concatWithBuilder2 = 46 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | 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,您可能需要检查ApacheCommonsLang和StringUtils.join()(以不同的风格)?
不管性能如何,它都可以节省您创建StringBuilder和循环的时间,这似乎是第一百万次。
我认为我们应该使用StringBuilder附加方法。原因是
字符串连接每次都将创建一个新的字符串对象(因为字符串是不可变的对象),因此它将创建3个对象。
使用字符串生成器,将只创建一个对象[StringBuilder是多表的],并将附加更多的字符串。
对于这样的简单字符串,我更喜欢使用
1 | "string".concat("string").concat("string"); |
按照顺序,我认为构造字符串的首选方法是使用StringBuilder、String concat(),然后使用重载的+运算符。当处理大型字符串时,StringBuilder的性能会显著提高,就像使用+运算符会大大降低性能(随着字符串大小的增加,性能会呈指数级大幅降低)。使用.concat()的一个问题是它可以引发NullPointerExceptions。