non*_*tor 889 java string performance stringbuilder concatenation
鉴于toString()
下面的两个实现,首选哪一个:
public String toString(){
return "{a:"+ a + ", b:" + b + ", c: " + c +"}";
}
Run Code Online (Sandbox Code Playgroud)
要么
public String toString(){
StringBuilder sb = new StringBuilder(100);
return sb.append("{a:").append(a)
.append(", b:").append(b)
.append(", c:").append(c)
.append("}")
.toString();
}
Run Code Online (Sandbox Code Playgroud)
?
更重要的是,鉴于我们只有3个属性,它可能没有什么区别,但你会在什么时候从+
concat 切换到 StringBuilder
?
Mic*_*rdt 929
版本1更可取,因为它更短,编译器实际上将其转换为版本2 - 无论如何都没有性能差异.
更重要的是,鉴于我们只有3个属性,它可能没有什么区别,但你在什么时候从concat切换到builder?
在循环中连接的时候 - 这通常是编译器无法自行替换StringBuilder
的时候.
joe*_*ely 240
关键是你是在一个地方写一个连接还是随着时间累积它.
对于您给出的示例,明确使用StringBuilder没有意义.(查看第一个案例的编译代码.)
但是如果你在循环中构建一个字符串,请使用StringBuilder.
为了澄清,假设hugeArray包含数千个字符串,代码如下:
...
String result = "";
for (String s : hugeArray) {
result = result + s;
}
Run Code Online (Sandbox Code Playgroud)
与以下相比,这是非常浪费时间和内存的:
...
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
sb.append(s);
}
String result = sb.toString();
Run Code Online (Sandbox Code Playgroud)
tan*_*ens 74
我更喜欢:
String.format( "{a: %s, b: %s, c: %s}", a, b, c );
Run Code Online (Sandbox Code Playgroud)
...因为它简短易读.
除非你在具有非常高重复次数的循环中使用它并测量了性能差异,否则我不会对速度进行优化.
我同意,如果你必须输出很多参数,这个表格会让人感到困惑(就像其中一条评论所说).在这种情况下,我将切换到一个更易读的形式(可能使用apache-commons的ToStringBuilder - 取自matt b的答案)并再次忽略性能.
Omr*_*dan 71
在大多数情况下,您不会看到两种方法之间的实际差异,但很容易构建像这样的最坏情况:
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+="*";
}
}
Run Code Online (Sandbox Code Playgroud)
输出是:
slow elapsed 11741 ms
fast elapsed 7 ms
Run Code Online (Sandbox Code Playgroud)
问题是,+ =追加到一个字符串会重建一个新字符串,因此它会花费与字符串长度成线性的东西(两者的总和).
所以 - 对你的问题:
第二种方法会更快,但它的可读性和维护难度更低.正如我所说,在你的具体情况下,你可能不会看到差异.
per*_*ain 27
我还与我的老板发生冲突,关于是否使用append或+.他们正在使用Append(我仍然无法弄清楚他们每次创建新对象时都会说出来).所以我想做一些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());
}
}
Run Code Online (Sandbox Code Playgroud)
以上课程的反汇编出来了
.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
; ---------------------------------------------------------------------------
Run Code Online (Sandbox Code Playgroud)
从上面的两个代码可以看出迈克尔是对的.在每种情况下,只创建一个SB对象.
Zof*_*ren 27
从Java 1.5开始,简单的一行连接"+"和StringBuilder.append()生成完全相同的字节码.
因此,为了代码可读性,请使用"+".
2例外:
rin*_*rer 22
使用最新版本的Java(1.8),disassembly(javap -c
)显示了编译器引入的优化.+
同样sb.append()
会产生非常相似的代码.但是,如果我们+
在for循环中使用它,那么检查行为是值得的.
在for循环中使用+添加字符串
Java的:
public String myCatPlus(String[] vals) {
String result = "";
for (String val : vals) {
result = result + val;
}
return result;
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
使用stringbuilder.append添加字符串
Java的:
public String myCatSb(String[] vals) {
StringBuilder sb = new StringBuilder();
for(String val : vals) {
sb.append(val);
}
return sb.toString();
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
但是有一些明显的区别.在第一种情况下,使用where +
,StringBuilder
为每个for循环迭代创建new ,并通过toString()
调用(29到41)存储生成的结果.因此,您正在生成使用+
operator in for
循环时真正不需要的中间字符串.
出于性能原因,不鼓励使用+=
(String
串联).之所以是:Java的String
是不可变的,每一个新的拼接完成一个新的时间String
创建(新建一个具有自已的旧的一个不同的指纹在字符串池).创建新字符串会给GC带来压力并减慢程序速度:对象创建很昂贵.
下面的代码应该使它更加实用和清晰.
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);
}
Run Code Online (Sandbox Code Playgroud)
运行结果报告如下.
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
Run Code Online (Sandbox Code Playgroud)
不考虑1次连接的结果(JIT尚未完成其工作),即使对于10次连接,性能损失也是相关的; 对于成千上万的连接,差异是巨大的.
从这个非常快速的实验中学到的经验(使用上面的代码很容易重现):永远不要使用+=
连接字符串,即使在需要一些连接的非常基本的情况下(如上所述,创建新的字符串无论如何都是昂贵的并且对GC).
Apache Commons-Lang有一个ToStringBuilder类,非常容易使用.它既可以处理append-logic,也可以格式化toString的外观.
public void toString() {
ToStringBuilder tsb = new ToStringBuilder(this);
tsb.append("a", a);
tsb.append("b", b)
return tsb.toString();
}
Run Code Online (Sandbox Code Playgroud)
将返回看起来像的输出com.blah.YourClass@abc1321f[a=whatever, b=foo]
.
或者使用链接以更精简的形式:
public void toString() {
return new ToStringBuilder(this).append("a", a).append("b", b").toString();
}
Run Code Online (Sandbox Code Playgroud)
或者,如果您想使用反射来包含该类的每个字段:
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
Run Code Online (Sandbox Code Playgroud)
如果需要,您还可以自定义ToString的样式.
小智 6
我认为我们应该采用 StringBuilder 附加方法。原因是:
String 连接每次都会创建一个新的字符串对象(由于 String 是不可变对象),因此会创建 3 个对象。
使用字符串生成器,只会创建一个对象[StringBuilder 是可变的],并且将进一步的字符串附加到该对象。
请参阅以下示例:
static final int MAX_ITERATIONS = 50000;
static final int CALC_AVG_EVERY = 10000;
public static void main(String[] args) {
printBytecodeVersion();
printJavaVersion();
case1();//str.concat
case2();//+=
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();
}
}
Run Code Online (Sandbox Code Playgroud)
输出:
java字节码版本:8
java.version:1.8.0_144
[str1.concat(str2)]
10000个并置的平均时间:0.096 ms
10000个并置的平均时间:0.185 ms平均
10000个并置的平均时间:0.327 ms平均并置的
时间10000个连接:10000个连接的
平均时间为0.501 ms 平均时间
创建的字符串长度:1950000 in 17745 ms
[str1 + = str2]
10000个连接的平均时间:0.21 ms平均
10000个连接的平均时间:0.652 ms
平均时间10000个串联:
平均1.129毫秒10000个串联的平均时间:1.727毫秒平均
10000个连接的平均时间:2.302 ms平均
创建的字符串长度:1950000 in 60279 ms
[str1.append(str2)]
10000个连接的平均时间:0.002 ms平均
10000个连接的平均时间:0.002 ms平均
10000个连接的平均时间: 0.002毫秒AVG
平均时间10000个级联:0.002毫秒AVG
平均时间10000个级联:0.002毫秒AVG
创建长度的字符串:1950000在100毫秒
随着字符串长度的增加,连接时间也会增加。
那是StringBuilder
绝对需要的地方。
如您所见,串联:UUID.randomUUID()+"---"
并没有真正影响时间。
PS:我不认为何时在Java中使用StringBuilder确实是重复的。
这个问题讨论的是toString()
,大多数时候哪个时间不执行大型字符串的连接。
2019更新
由于java8
时间,事情已经改变了一点。看来now(java13)的串联时间+=
实际上与相同str.concat()
。但是,StringBuilder
串联时间仍然是恒定的。(上面的原始帖子经过了稍微的编辑,以添加更多详细的输出)
java字节码版本:13
java.version:13.0.1
[str1.concat(str2)]
10000个连接的平均时间:0.047 ms
10000个连接的平均时间:0.1 ms avg
10000个连接的平均时间:0.17 ms avg的
平均时间10000个连接:10000个连接的
平均时间为0.255 ms 平均时间
创建的字符串长度:1950000 in 9147 ms
[str1 + = str2]
10000个连接的平均时间:0.037 ms平均
10000个连接的平均时间:0.097 ms
平均时间10000次串联:
平均0.249毫秒10000次串联的平均时间:0.298毫秒平均
10000个连接的平均时间:0.326 ms平均
创建的字符串长度:1950000 in 10191 ms
[str1.append(str2)]
10000个连接的平均时间:0.001 ms平均
10000个连接的平均时间:0.001 ms平均
10000个连接的平均时间: 0.001毫秒AVG
平均时间10000个级联:0.001毫秒AVG
平均时间10000个级联:0.001毫秒AVG
创建长度的字符串:1950000在43毫秒
值得注意的是,bytecode:8/java.version:13
与之相比,组合具有良好的性能优势bytecode:8/java.version:8
归档时间: |
|
查看次数: |
344557 次 |
最近记录: |