IT-Swarm.Net

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

鉴于下面的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

845
non sequitor

版本1是更好的,因为它更短和 编译器实际上将它转换为版本2 - 没有任何性能差异。

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

在循环中连接的时候 - 通常编译器不能单独替换StringBuilder

880
Michael Borgwardt

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

对于您给出的示例,明确使用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();
225
joel.neely

我更喜欢:

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

...因为它简短易读。

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

我同意,如果你必须输出很多参数,这个表格会让人感到困惑(就像其中一条评论所说)。在这种情况下,我将切换到一个更易读的形式(可能使用 ToStringBuilder Apache-commons - 取自matt b的答案)并再次忽略性能。

68
tangens

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

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

问题是,+ =追加到一个字符串会重建一个新字符串,因此它会花费与字符串长度成线性的东西(两者的总和)。

所以 - 对你的问题:

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

63
Omry Yadan

我还与我的老板发生冲突,关于是否使用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());
    }
}

以上课程的反汇编出来了

 .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对象。

27
perilbrain

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

因此,为了代码可读性,请使用“+”。

2例外:

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

使用最新版本的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 loop excerpt)

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添加字符串

Java的:

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

ByteCdoe:(for loop excerpt)

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循环中使用+运算符时,您正在生成您真正不需要的中间字符串。

21
ring bearer

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

我们的想法是用对Java.lang.invoke.StringConcatFactory的简单invokedynamic调用替换整个StringBuilder追加舞蹈,它将接受需要连接的值。

8
ZhekaKozlov

出于性能原因,不建议使用+=String concatenation)。原因是: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);
}

运行结果报告如下。

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)。

7
Paolo Maresca

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的样式。

7
matt b

尽可能使toString方法可读!

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

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

4
Thorbjørn Ravn Andersen

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

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

所以区别很大。循环内部StringBuilder要快得多。

3
Eddy

性能明智使用'+'进行字符串连接是比较昂贵的,因为它必须创建一个全新的String副本,因为字符串在Java中是不可变的。如果连接非常频繁,这会发挥特殊作用,例如:在循环内部。以下是我的IDEA建议当我尝试做这样的事情时:

enter image description here

通用规则:

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

这是一个 Nice Jon Skeet博客 围绕这个主题。

2
Sudip Bhandari

请参阅以下示例:

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

输出:

10000个连接的平均时间:平均0.096毫秒
10000个连接的平均时间:平均0.185毫秒
10000个连接的平均时间:平均0.327毫秒
10000个连接的平均时间:平均0.501毫秒
10000个连接的平均时间:平均0.656毫秒
创建的字符串长度:1950000 in 17745 ms
10000个连接的平均时间:平均0.21毫秒
10000个连接的平均时间:平均0.652毫秒
10000个连接的平均时间:平均1.129毫秒
10000个连接的平均时间:平均1.727毫秒
10000个连接的平均时间:平均2.302毫秒
创建的字符串长度:1950000 in 60279 ms
10000个连接的平均时间:平均0.002毫秒
10000个连接的平均时间:平均0.002毫秒
10000个连接的平均时间:平均0.002毫秒
10000个连接的平均时间:平均0.002毫秒
10000个连接的平均时间:平均0.002毫秒
创建的字符串长度:1950000 in 100 ms

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

P.S。: 我不认为 何时在Java中使用StringBuilder 实际上是与此重复。
这个问题谈论toString(),大多数时候都没有执行大字符串的连接。

2
Marinos An

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

无论性能如何,它都可以节省您必须创建StringBuilders和for循环,看起来像 millionth time。

1
Brian Agnew

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