For some reason, the designers of Java decided to make +
and +=
work with Strings. These are the only Objects in Java that have an overloaded operator that turns out to do some magic behind the scenes.
You see it all over the place:
1 |
String foo = bar + qux + "\n" + 42; |
But when is this acceptable when when is it problematic?
The evils of +=
Lets take a simple class with some String operations.
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 |
public class Main { public static String simpleLoop() { String foo = ""; StringBuilder bar = new StringBuilder(); String qux = ""; String baz = ""; for(int i = 0; i < 100; i++) { foo += i; foo += i; bar.append(i); foo += i; qux += i; baz = baz.concat(Integer.toString(i)); } return foo.concat(bar.toString()).concat(qux).concat(baz); } public static String lotsOfPluses(String[] args) { String foo = ""; StringBuilder bar = new StringBuilder(); for(int i = 0; i < args.length; i++) { foo += args[i] + i; bar.append(args[i]).append(i); } return foo.concat(bar.toString()); } public static void main(String[] args) { System.out.println(simpleLoop()); System.out.println(lotsOfPluses(args)); } } |
Please ignore the return
s and main
– they’re just there to make it so I have something my IDE doesn’t complain about and to make sure I’m not missing obvious things.
The worst offender for a +=
is the +=
in a loop. Looking at line 12, we can see:
1 2 3 4 5 6 7 8 9 10 11 |
L11 LINENUMBER 12 L11 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 2 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 3 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 2 |
When you do a += with a String, you:
- create a StringBuilder
- append the LHS (left hand side) to it
- append whatever you’ve got on the RHS (right hand side)
- invoke toString on the String, and then store that back in the original LHS
Note that at this point the original LHS is now has no references and the StringBuilder that was created for this has no references. We’ve created and disposed of two objects. While this isn’t that bad (they’ll get garbage collected soon enough), its still not a good thing.
Also consider the size of the objects being collected. In the past I’ve come across code that looked very much like this that was creating a String that was a few kilobytes in size character by character in a loop. Near the end of the run the the previously used (and now discarded) String and StringBuilder were each creating (copying, appending) and disposing of dozens of kilobytes of objects every iteration. While garbage collecting can handle objects that are disposed of quickly (lots of little objects) these objects weren’t little anymore.
On the other hand, line 10 shows what goes on with a StringBuilder doing the same operation.
1 2 3 4 5 6 |
L8 LINENUMBER 10 L9 ALOAD 1 ILOAD 3 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; POP |
In this case:
- append is invoked on the StringBuilder
That is a much simpler process. Realize that there’s a StringBuilder that was initialized up at the top, and to get it back out of the StringBuilder, you still need to invoke toString()
on it. But that was only done once.
There’s another option for something to consider when you are doing this once (and by once, I mean once and only once – not once in each loop iteration). That is String.concat(String str). The code for String.concat(String str) and StringBuilder.append(String str) are really quite similar. StringBuilder.append(int i) has some optimizations specific to integers (or longs, or doubles, or chars) – but it still boils down to very similar code.
The byte code for String.concat of it is also similar:
1 2 3 4 5 6 7 |
L12 LINENUMBER 13 L12http://shagie.net/wp-admin/post-new.php# ALOAD 3 ILOAD 4 INVOKESTATIC java/lang/Integer.toString (I)Ljava/lang/String; INVOKEVIRTUAL java/lang/String.concat (Ljava/lang/String;)Ljava/lang/String; ASTORE 3 |
- convert the Integer to a String
- concatenate the Strings together
- store the resulting String
Note that we’re still creating a String for the integer and discarding the old LHS. In terms of Object creation this is on par with a StringBuilder to append two items together. Note that this isn’t a solution to the issue of +=
in a loop because we are still creating two additional Strings each loop, though memory use would be lower.
So, what optimizations are done with +=
when creating strings?
Lets look at a segment of code in there where a value is being appended to a String with +=
multiple times in the same scope.
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 |
L7 LINENUMBER 8 L7 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 4 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 0 L8 LINENUMBER 9 L8 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 4 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 0 L9 LINENUMBER 10 L9 ... L10 LINENUMBER 11 L10 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 0 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 4 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 0 |
Here you can see line 8 setting up the StringBuilder appending, and then storing it. Line 9 creates a new StringBuilder, appends, and then stores. Line 11 creates a new StringBuilder, appends and stores.
There are no optimizations being done on the following code:
8 9 10 11 |
foo += i; foo += i; bar.append(i); foo += i; |
Multiple +=
invocations are no better than a single one, and clearly a situation where it shouldn’t be done this way. Each line is creating creating a StringBuilder and a String and throwing away the old String and StringBuilder.
That was painful to read. What about the use of +
on the same line?
This takes us down to line 23 and 24.
23 24 |
foo += args[i] + i; bar.append(args[i]).append(i); |
The byte code for this is interesting because there is an optimization being done here.
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 |
L5 LINENUMBER 23 L5 NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ALOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ALOAD 0 ILOAD 3 AALOAD INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 3 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; ASTORE 1 L6 LINENUMBER 24 L6 ALOAD 2 ALOAD 0 ILOAD 3 AALOAD INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 3 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; POP |
Again, the new StringBuilder is a wash as is the conversion from a StringBuilder to a String. However, it is clear that when you have String foo = bar + qux + 1 + '\n';
the resulting code will be approximately the same as if you were to create a StringBuilder, call append on it and then convert it back to a String.
Conclusion
- If you have one
+=
, consider.concat
instead. - If you have two
+=
, you are better off creating your own StringBuilder. - If you have one line with
String foo = a + b + c + d;
, it will be the same as if you create your own StringBuilder.
code
more code
~~~~