关于性能:StringBuilder vs Java中toString()的字符串连接

StringBuilder vs String concatenation in toString() in Java

Implementations below,which one is preferred:

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

黄金

ZZU1

更重要的是,给我们的只有三种性能,这可能不是一种不同,但你从+开关到StringBuilder有什么用?


版本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没有意义。(查看第一个案例的编译代码。)

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

要澄清,假设hugearray包含数千个字符串,请执行以下代码:

1
2
3
4
5
...
String result ="";
for (String s : hugeArray) {
    result = result + s;
}

与以下情况相比,时间和记忆非常浪费:

1
2
3
4
5
6
...
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
    sb.append(s);
}
String result = sb.toString();


我更喜欢:

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

…因为它简短易读。

我不会在速度上对此进行优化,除非您在具有非常高重复计数的循环中使用它并测量了性能差异。

我同意,如果必须输出大量参数,那么这个表单可能会变得混乱(就像其中一条评论所说)。在本例中,我将切换到更可读的形式(可能使用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)表示编译器所介绍的优化。+sb.append()将生成非常相似的代码。但是,如果我们在for循环中使用+,那么检查行为是值得的。

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

爪哇:

1
2
3
4
5
6
7
public String myCatPlus(String[] vals) {
    String result ="";
    for (String val : vals) {
        result = result + val;
    }
    return result;
}

字节码:(for循环摘录)

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
public String myCatSb(String[] vals) {
    StringBuilder sb = new StringBuilder();
    for(String val : vals) {
        sb.append(val);
    }
    return sb.toString();
}

BYTECDOE:(for循环摘录)

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

不过有一点明显的区别。在第一种情况下,使用+时,为每个for循环迭代创建新的StringBuilder,生成的结果通过执行toString()调用(29到41)来存储。因此,在for循环中使用+操作符时,生成了真正不需要的中间字符串。


在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.


出于性能原因,不鼓励使用+=(String串联)。原因是:JavaEDCOX1 12是一个不可变的,每次进行一个新的连接时,就会创建一个新的EDCOX1×12(新的一个与已经在字符串池中的旧的指纹有不同的指纹)。创建新的字符串会给GC带来压力,并减慢程序的速度:对象创建成本很高。

下面的代码应该使它更实际和清晰的同时。

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个连接,性能惩罚也是相关的;对于数千个连接,差异是巨大的。

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


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();
}

将返回类似于com.blah.YourClass@abc1321f[a=whatever, b=foo]的输出。

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

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

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

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

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


尽可能使ToString方法可读!

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

还要注意的是,Java 5编译器生成的代码比在早期版本的Java中使用的手写"StringBuffer"方法更快。如果您使用"+"这个和未来的增强是免费的。


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

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

所以差别很大。在一个循环中,StringBuilder要快得多。


使用"+"的性能明智的字符串连接是昂贵的,因为它必须生成一个新的字符串副本,因为字符串在Java中是不可变的。如果连接非常频繁,例如:在循环中,这将起到特殊的作用。当我尝试做这样的事情时,我的想法建议如下:

enter image description here

一般规则:

  • 在单个字符串分配中,可以使用字符串串联。
  • 如果要循环生成一大块字符数据,请使用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

随着字符串长度的增加,连接时间也会增加。这正是StringBuilder绝对需要的地方。如您所见,串联:UUID.randomUUID()+"---"并不真正影响时间。

P.S.:我不认为何时在Java中使用StrugBu建器实际上是一个复制品。这个问题讨论的是toString(),大多数时候它不执行大型字符串的串联。


我比较了四种不同的方法来比较性能。我完全不知道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。