Sitemap
Kamil Lolo

Kamil Lolo

Just a programmer :)

StringBuilder and Java compiler trap

3 min readFeb 22, 2024

Is it worth worrying about string concatenation in Java?

Press enter or click to view image in full size
Photo by Nick Fewings on Unsplash

Do you use StringBuilder to concatenate strings? Most of you have probably encountered the statement that, for optimizing string concatenation operations, it is advisable to use StringBuilder or the thread-safe StringBuffer. But is this advice always correct?

Get Kamil Lolo’s stories in your inbox

Join Medium for free to get updates from this writer.

What does StringBuilder offer? As we know, a string is an immutable object; you can't append additional characters to it. Every time you need to create a longer string, a new object is created. StringBuilder stores individual characters in an array, and new data is added to it during each append operation. Only when the toString method is called is the final string created, containing all the characters from the temporary array. If concatenation is performed in a loop, like here:

public static void main(final String... args) {
String result = "";
for (int i = 0; i < 100; i++) {
result += i + " ";
}

System.out.println(result);
}

With each iteration of the loop, we create an unnecessary object in memory for an increasingly longer string. This not only consumes memory but also time for data allocation and their subsequent garbage collection. So, you probably know that such a loop should look something like this:

public static void main(final String... args) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 100; i++) {
builder.append(i).append(" ");
}

System.out.println(builder.toString());
}

Using StringBuilder optimizes the code. However, such a solution requires a bit more writing and can sometimes obscure the code. Do we really have to write such code? Not necessarily! The documentation says:

An implementation (of compiler) may choose to perform conversion and concatenation in one step to avoid creating and then discarding an intermediate String object. To increase the performance of repeated string concatenation, a Java compiler may use the StringBuffer class or a similar technique to reduce the number of intermediate String objects that are created by evaluation of an expression. For primitive types, an implementation may also optimize away the creation of a wrapper object by converting directly from a primitive type to a string.

It’s interesting why this statement is in lowercase…? 😛 It follows that the compiler can automatically insert StringBuilder in place of string concatenation. Let's check it! Decompiling the code from listing 1 using the command javap -c Demo.class yields:

public static void main(java.lang.String...);
Code:
0: ldc #2 // String
2: astore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 100
8: if_icmpge 41
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: aload_1
19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: iload_2
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26: ldc #7 // String
28: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: astore_1
35: iinc 2, 1
38: goto 5
41: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
44: aload_1
45: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: return

Indeed, magically, the string builder appears in the generated bytecode.
Pay attention to line 15, where the StringBuilder constructor is called, and on line 38, where there is a goto instruction to line 5, where the loop begins.

So, how does the code from listing 2, where I explicitly used StringBuilder, look like?

public static void main(java.lang.String...);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: bipush 100
13: if_icmpge 33
16: aload_1
17: iload_2
18: invokevirtual #4 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
21: ldc #5 // String
23: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26: pop
27: iinc 2, 1
30: goto 10
33: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_1
37: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
40: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: return

The code in both cases looks similar. However, in the case of the code where I explicitly used StringBuilder, the StringBuilder object is created only once. In contrast, in the code optimized by the compiler, it is created with each iteration of the loop. Thus, not only does an unnecessary string in memory get created with each loop iteration, but also an unnecessary StringBuilder object is created. This shows us that we should not blindly trust the compiler. It's worth delving into the bytecode from time to time and considering whether the written code is actually optimal.

--

--