开发者

StringBuilder vs String concatenation in toString() in Java

开发者 https://www.devze.com 2022-12-08 02:38 出处:网络
Given the 2 toString() implementations below, which one is preferred: public String toString(){ return \"{a:\"+ a + \", b:\" + b + \", c: \" + c +\"}\";

Given the 2 toString() implementations below, which one is preferred:

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

or

public String toString(){
    StringBuilder sb = new StringBuilder(100);
    return sb.append("{a:").append(a)
          .append(", b:").app开发者_StackOverflowend(b)
          .append(", c:").append(c)
          .append("}")
          .toString();
}

?

More importantly, given we have only 3 properties it might not make a difference, but at what point would you switch from + concat to StringBuilder?


Version 1 is preferable because it is shorter and the compiler will in fact turn it into version 2 - no performance difference whatsoever.

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?

At the point where you're concatenating in a loop - that's usually when the compiler can't substitute StringBuilder by itself.


The key is whether you are writing a single concatenation all in one place or accumulating it over time.

For the example you gave, there's no point in explicitly using StringBuilder. (Look at the compiled code for your first case.)

But if you are building a string e.g. inside a loop, use StringBuilder.

To clarify, assuming that hugeArray contains thousands of strings, code like this:

...
String result = "";
for (String s : hugeArray) {
    result = result + s;
}

is very time- and memory-wasteful compared with:

...
StringBuilder sb = new StringBuilder();
for (String s : hugeArray) {
    sb.append(s);
}
String result = sb.toString();


In most cases, you won't see an actual difference between the two approaches, but it's easy to construct a worst case scenario like this one:

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+="*";
    }
}

The output is:

slow elapsed 11741 ms
fast elapsed 7 ms

The problem is that to += append to a string reconstructs a new string, so it costs something linear to the length of your strings (sum of both).

So - to your question:

The second approach would be faster, but it's less readable and harder to maintain. As I said, in your specific case you would probably not see the difference.


I prefer:

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

...because it's short and readable.

I would not optimize this for speed unless you use it inside a loop with a very high repeat count and have measured the performance difference.

I agree, that if you have to output a lot of parameters, this form can get confusing (like one of the comments say). In this case I'd switch to a more readable form (perhaps using ToStringBuilder of apache-commons - taken from the answer of matt b) and ignore performance again.


Since Java 1.5, simple one line concatenation with "+" and StringBuilder.append() generate exactly the same bytecode.

So for the sake of code readability, use "+".

2 exceptions :

  • multithreaded environment : StringBuffer
  • concatenation in loops : StringBuilder/StringBuffer


I also had clash with my boss on the fact whether to use append or +.As they are using Append(I still cant figure out as they say every time a new object is created). So I thought to do some R&D.Although I love Michael Borgwardt explaination but just wanted to show an explanation if somebody will really need to know in future.

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

and disassembly of above class comes out as

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

From the above two codes you can see Michael is right.In each case only one SB object is created.


Using latest version of Java(1.8) the disassembly(javap -c) shows the optimization introduced by compiler. + as well sb.append() will generate very similar code. However, it will be worthwhile inspecting the behaviour if we are using + in a for loop.

Adding strings using + in a for loop

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

Adding strings using 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

There is a bit of glaring difference though. In first case, where + was used, new StringBuilder is created for each for loop iteration and generated result is stored by doing a toString() call(29 through 41). So you are generating intermediate Strings that your really do not need while using + operator in for loop.


It depends on the size of string.

See the example below:

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

Output:

java bytecode version:8
java.version: 1.8.0_144
[str1.concat(str2)]
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
[str1+=str2]
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
[str1.append(str2)]
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

As the string length increases, so does the += and .concat concatenation times, with the latter being more efficient but still non-constant
That is where the StringBuilder is definitely needed.

P.S.: I don't think When to use StringBuilder in Java is really a duplicate of this.
This question talks about toString() which most of the times does not perform concatenations of huge strings.


2019 Update

Since java8 times, things have changed a bit. It seems that now(java13), the concatenation time of += is practically the same as str.concat(). However StringBuilder concatenation time is still constant. (Original post above was slightly edited to add more verbose output)

java bytecode version:13
java.version: 13.0.1
[str1.concat(str2)]
average time for 10000 concatenations: 0.047 ms avg
average time for 10000 concatenations: 0.1 ms avg
average time for 10000 concatenations: 0.17 ms avg
average time for 10000 concatenations: 0.255 ms avg
average time for 10000 concatenations: 0.336 ms avg
Created string of length:1950000 in 9147 ms
[str1+=str2]
average time for 10000 concatenations: 0.037 ms avg
average time for 10000 concatenations: 0.097 ms avg
average time for 10000 concatenations: 0.249 ms avg
average time for 10000 concatenations: 0.298 ms avg
average time for 10000 concatenations: 0.326 ms avg
Created string of length:1950000 in 10191 ms
[str1.append(str2)]
average time for 10000 concatenations: 0.001 ms avg
average time for 10000 concatenations: 0.001 ms avg
average time for 10000 concatenations: 0.001 ms avg
average time for 10000 concatenations: 0.001 ms avg
average time for 10000 concatenations: 0.001 ms avg
Created string of length:1950000 in 43 ms

Worth noting also bytecode:8/java.version:13 combination has a good performance benefit compared to bytecode:8/java.version:8


In Java 9 the version 1 should be faster because it is converted to invokedynamic call. More details can be found in 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.


For performance reasons, the use of += (String concatenation) is discouraged. The reason why is: Java String is an immutable, every time a new concatenation is done a new String is created (the new one has a different fingerprint from the older one already in the String pool ). Creating new strings puts pressure on the GC and slows down the program: object creation is expensive.

Below code should make it more practical and clear at the same time.

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

Results for a run are reported below.

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

Not considering the results for 1 concatenation (JIT was not yet doing its job), even for 10 concatenations the performance penalty is relevant; for thousands of concatenations, the difference is huge.

Lessons learned from this very quick experiment (easily reproducible with the above code): never use the += to concatenate strings together, even in very basic cases where a few concatenations are needed (as said, creating new strings is expensive anyway and puts pressure on the GC).


Apache Commons-Lang has a ToStringBuilder class which is super easy to use. It does a nice job of both handling the append-logic as well as formatting of how you want your toString to look.

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

Will return output that looks like com.blah.YourClass@abc1321f[a=whatever, b=foo].

Or in a more condensed form using chaining:

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

Or if you want to use reflection to include every field of the class:

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

You can also customize the style of the ToString if you want.


I think we should go with StringBuilder append approach. Reason being :

  1. The String concatenate will create a new string object each time (As String is immutable object) , so it will create 3 objects.

  2. With String builder only one object will created[StringBuilder is mutable] and the further string gets appended to it.


Make the toString method as readable as you possibly can!

The sole exception for this in my book is if you can prove to me that it consumes significant resources :) (Yes, this means profiling)

Also note that the Java 5 compiler generates faster code than the handwritten "StringBuffer" approach used in earlier versions of Java. If you use "+" this and future enhancements comes for free.


There seems to be some debate whether using StringBuilder is still needed with current compilers. So I thought I'll give my 2 cents of experience.

I have a JDBC result set of 10k records (yes, I need all of them in one batch.) Using the + operator takes about 5 minutes on my machine with Java 1.8. Using stringBuilder.append("") takes less than a second for the same query.

So the difference is huge. Inside a loop StringBuilder is much faster.


Here is what I checked in Java8

  • Using String concatenation
  • Using StringBuilder

    long time1 = System.currentTimeMillis();
    usingStringConcatenation(100000);
    System.out.println("usingStringConcatenation " + (System.currentTimeMillis() - time1) + " ms");
    
    time1 = System.currentTimeMillis();
    usingStringBuilder(100000);
    System.out.println("usingStringBuilder " + (System.currentTimeMillis() - time1) + " ms");
    
    
    private static void usingStringBuilder(int n)
    {
        StringBuilder str = new StringBuilder();
        for(int i=0;i<n;i++)
            str.append("myBigString");    
    }
    
    private static void usingStringConcatenation(int n)
    {
        String str = "";
        for(int i=0;i<n;i++)
            str+="myBigString";
    }
    

It's really a nightmare if you are using string concatenation for large number of strings.

usingStringConcatenation 29321 ms
usingStringBuilder 2 ms


I think this image would be very useful to compare all classes for working with Strings:

StringBuilder vs String concatenation in toString() in Java


It's worth mentioning that as pointed out by @ZhekaKozlov,

+ is faster since Java 9, unless JVM doesn't know how to optimise it (e.g. concatenation in a loop).

I checked the bytecode for following code (in Java 17):

public class StringBM {
    public String toStringPlus(String a) {
        return "{a:" + a + ", b:" + ", c: " + "}";
    }

    public String toStringBuilder(String a) {
        StringBuilder sb = new StringBuilder(100);
        return sb.append("{a:").append(a)
                .append(", b:")
                .append(", c:")
                .append("}")
                .toString();
    }
}

For toStringPlus:

 0: aload_1
 1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
 6: areturn

for toStringBuilder:

 0: new           #11                 // class java/lang/StringBuilder
 3: dup
 4: bipush        100
 6: invokespecial #13                 // Method java/lang/StringBuilder."<init>":(I)V
 9: astore_2
10: aload_2
11: ldc           #16                 // String {a:
13: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: aload_1
17: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: ldc           #22                 // String , b:
22: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: ldc           #24                 // String , c:
27: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: ldc           #26                 // String }
32: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
35: invokevirtual #28                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
38: areturn

The + version simply invokes dynamic function makeConcatWithConstants and and pass in the method argument {a:\u0001, b:, c: } (\u0001 being the parameter placeholder).
Whereas the StringBuilder version has to do it the 'honest' way.
I guess we can see why is + faster now.


Can I point out that if you're going to iterate over a collection and use StringBuilder, you may want to check out Apache Commons Lang and StringUtils.join() (in different flavours) ?

Regardless of performance, it'll save you having to create StringBuilders and for loops for what seems like the millionth time.


Performance wise String concatenation using '+' is costlier because it has to make a whole new copy of String since Strings are immutable in java. This plays particular role if concatenation is very frequent, eg: inside a loop. Following is what my IDEA suggests when I attempt to do such a thing:

StringBuilder vs String concatenation in toString() in Java

General Rules:

  • Within a single string assignment, using String concatenation is fine.
  • If you're looping to build up a large block of character data, go for StringBuffer.
  • Using += on a String is always going to be less efficient than using a StringBuffer, so it should ring warning bells - but in certain cases the optimisation gained will be negligible compared with the readability issues, so use your common sense.

Here is a nice Jon Skeet blog around this topic.


For simple strings like that I prefer to use

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

In order, I would say the preferred method of constructing a string is using StringBuilder, String#concat(), then the overloaded + operator. StringBuilder is a significant performance increase when working large strings just like using the + operator is a large decrease in performance (exponentially large decrease as the String size increases). The one problem with using .concat() is that it can throw NullPointerExceptions.

0

精彩评论

暂无评论...
验证码 换一张
取 消