The + operator on strings and a StringBuilder can produce the same text, but they do it in completely different ways. Concatenation with + creates a brand new string every time, while StringBuilder keeps one mutable buffer and edits it in place. For a couple of joins they behave the same; inside a loop the difference decides whether your program is fast or slow.
What does the plus operator actually do?
When you write a + b with strings, Java does not "add" anything. It calls a method that produces a new string containing the characters of a followed by the characters of b. The two original strings are untouched, and the result is a third object that must be allocated and filled.
public class ConcatIdentity {
public static void main(String[] args) {
String base = "ab";
String plus = base + "cd";
String other = "abcd";
System.out.println("plus equals other : " + plus.equals(other));
System.out.println("plus == other : " + (plus == other));
String literal = "ab" + "cd";
System.out.println("literal == other : " + (literal == other));
String variable = "ab";
String runtimeConcat = variable + "cd";
System.out.println("runtimeConcat == other : " + (runtimeConcat == other));
System.out.println("runtimeConcat identity : " + System.identityHashCode(runtimeConcat));
System.out.println("other identity : " + System.identityHashCode(other));
}
}plus equals other : true
plus == other : false
literal == other : true
runtimeConcat == other : false
runtimeConcat identity : 1927950199
other identity : 868693306Two of those lines are worth pausing on. literal == other is true because the compiler folds two constant literals into one at compile time, so no runtime concatenation happens. runtimeConcat == other is false because one operand was a variable, so the concatenation ran at runtime and produced a fresh object with a different identity hash. The texts are equal, but they are not the same object - which is the same lesson as equals versus ==, seen from the other side.
Why does concatenation hurt so much inside a loop?
Imagine appending twenty thousand characters with text += 'x'. On every iteration Java allocates a new string that holds everything written so far, copies all of it, and hands it back. The first iteration copies one character, the second copies two, and the twentieth thousand copies twenty thousand. The total amount of copying grows with the square of the final length, and each intermediate string becomes garbage the moment the loop moves on.
StringBuilder avoids all of that. It holds a character buffer that grows as needed, and append writes into that buffer and returns the same builder. There is no copy of the previous contents, no new object per iteration, and no garbage pile-up.
public class BuilderVsPlus {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
for (int i = 1; i <= 5; i++) {
builder.append(i).append(' ');
}
System.out.println("builder : [" + builder + "]");
System.out.println("length : " + builder.length());
StringBuilder loop = new StringBuilder();
long start = System.nanoTime();
for (int i = 0; i < 20000; i++) {
loop.append('x');
}
long builderTime = System.nanoTime() - start;
String plus = "";
start = System.nanoTime();
for (int i = 0; i < 20000; i++) {
plus += 'x';
}
long plusTime = System.nanoTime() - start;
System.out.println("builder chars : " + builder.length() + ", plus chars : " + plus.length());
System.out.println("builder was faster on this run : " + (builderTime < plusTime));
System.out.println("same content : " + plus.contentEquals(loop));
}
}builder : [1 2 3 4 5 ]
length : 10
builder chars : 10, plus chars : 20000
builder was faster on this run : true
same content : trueThe timings printed here are from a single run on a single machine, so treat them as a demonstration rather than a benchmark - do not quote them as a performance figure. What is guaranteed is the shape of the work: the builder writes each character once, while repeated += copies the accumulated text over and over. The final line is the important one, proving both approaches produced identical content.
Which StringBuilder methods do you need?
append is the workhorse, and it returns the builder so calls can be chained. insert, delete and replace edit at a position, reverse flips the content, and setLength truncates or extends it. length and charAt let you inspect the result. Every one of these mutates the builder in place, so there is exactly one object involved.
public class BuilderApi {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("hello");
sb.append(' ').append("world");
System.out.println("after append : " + sb);
sb.insert(0, ">> ");
System.out.println("after insert : " + sb);
sb.delete(0, 3);
System.out.println("after delete : " + sb);
sb.replace(0, 5, "HELLO");
System.out.println("after replace : " + sb);
System.out.println("reversed : " + sb.reverse());
sb.setLength(5);
System.out.println("after setLength : " + sb + " (length " + sb.length() + ")");
System.out.println("charAt(0) : " + sb.charAt(0));
System.out.println("indexOf L : " + sb.indexOf("L"));
}
}after append : hello world
after insert : >> hello world
after delete : hello world
after replace : HELLO world
reversed : dlrow OLLEH
after setLength : dlrow (length 5)
charAt(0) : d
indexOf L : -1setLength is the one that surprises people. Shortening the builder throws the extra characters away; lengthening it fills the gap with null characters, which is rarely what you want but is occasionally useful when you are working with a fixed-size buffer.
What is StringBuffer, and does it matter?
StringBuffer is the older, thread-safe sibling of StringBuilder. Every operation on it is synchronised, which is what makes it safe to share between threads, and also what makes it slower for the single-threaded case that almost all code is.
public class BufferCompare {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder("abc");
java.lang.StringBuffer buffer = new java.lang.StringBuffer("abc");
System.out.println("builder class : " + builder.getClass().getName());
System.out.println("buffer class : " + buffer.getClass().getName());
System.out.println("buffer length : " + buffer.length());
StringBuilder joined = new StringBuilder();
for (String part : new String[] { "x", "y", "z" }) {
joined.append(part);
}
System.out.println("joined : " + joined);
System.out.println("toString type : " + joined.toString().getClass().getName());
}
}builder class : java.lang.StringBuilder
buffer class : java.lang.StringBuffer
buffer length : 3
joined : xyz
toString type : java.lang.StringBecause String itself is immutable, building a string in shared state is safe without any locking at all - you build it locally and publish the finished result. So the rule is simple: use StringBuilder in application code, and reach for StringBuffer only if you have genuinely shared a mutable builder between threads, which is a design smell in any case.
Which should you use, and when?
| Situation | Use | Reason |
|---|---|---|
| Joining two or three pieces | + or String.join | Simple, and the cost is irrelevant |
| Building text in a loop | StringBuilder | One buffer, no repeated copying |
| Many pieces at once | String.join | Does the sizing for you |
| One string from a stream | collect(joining()) | Fits the pipeline style |
| Shared across threads | StringBuffer or build locally | Synchronised, but usually avoidable |
One more idiom is worth memorising because it appears in almost every interview: when you do not know the final size, size the builder up front. new StringBuilder(estimatedLength) avoids a few internal copies while the buffer grows. Getting the estimate roughly right is enough - the builder still grows if you are wrong.
The rules to remember
- Concatenation with
+always allocates a new string; the builder edits in place. - Inside a loop,
+=copies the accumulated text again and again - use a builder. appendreturns the builder, so calls chain naturally.- Every builder method mutates the same object, so keep a reference and call
toStringat the end. - Prefer
String.joinwhen you already have the pieces in an array or collection.
Try it yourself: write a method that takes a list of words and returns them joined by a comma, first with a StringBuilder and then with a plain loop that uses +=. Time both with System.nanoTime() on a list of fifty thousand words and print the two durations along with a check that both results are equal. Add a comment explaining why the numbers differ.