Strings: Immutability, the Pool and StringBuilder
Why String is immutable and what that buys you, how the string pool and intern() really work, why == sometimes appears to work, compact strings, and when concatenation in a loop actually costs you.
On this page
String questions look like beginner material and are asked at every level, because the follow-ups
run deep: from why immutability was chosen, through the mechanics of the pool, into a real
performance characteristic that still bites production code.
Key Takeaways
- Immutability is what makes strings safe to cache, share across threads and use as
HashMapkeys — the hash is computed once and stays valid. - The string pool holds literals and anything explicitly interned. It has lived on the heap since Java 7, not in PermGen.
==compares references. It appears to work for literals because they are pooled, which is exactly what makes the bug hard to spot.- Concatenating in a loop is quadratic. Java 9's
invokedynamicchange did not fix that. - Compact strings (Java 9+) store Latin-1 text one byte per character, typically cutting string memory by close to half.
Why immutable
String is final, its backing array is private final, and no method mutates it — every operation
returns a new instance. Four things follow.
Safe sharing. Any number of threads can read the same String with no synchronisation, because
there is no state transition to observe. That is also what makes pooling possible: if strings were
mutable, two variables sharing a pooled instance would see each other's edits.
Cached hash code. String.hashCode() computes once and stores the result in a field. Since the
content cannot change, the cached value can never be stale. That makes String the ideal HashMap
key, and it is why map-heavy code with string keys is fast.
Security. File paths, class names, database URLs and hostnames are all passed around as strings. If they were mutable, a caller could pass a validated path and then mutate it after the check but before the open — a time-of-check-to-time-of-use hole. Immutability makes validation meaningful.
Class loading. The JVM identifies classes by name. A mutable class name would be an obvious attack surface.
The string pool
The pool is a JVM-internal table of String instances, keyed by content. Every string literal in
compiled code is added to it during class loading, and any literal with the same content resolves to
the same instance.
String a = "spring";
String b = "spring";
a == b; // true — same pooled instance
String c = new String("spring");
a == c; // false — c is a distinct heap object
a.equals(c); // true
String d = c.intern();
a == d; // true — intern() returns the pooled instance
String e = "spr" + "ing"; // constant-folded by javac into the literal
a == e; // true
String part = "spr";
String f = part + "ing"; // built at runtime, not pooled
a == f; // falseLines four and eight are the whole lesson. "spr" + "ing" is folded by the compiler into a single
literal, so it is pooled. part + "ing" is a runtime concatenation producing a fresh object. Two
expressions that look identical in source have different identity — which is precisely why == on
strings is a bug even when it appears to work.
Since Java 7 the pool is a hash table in normal heap, so interned strings are garbage-collectable and
no longer risk filling PermGen. Its bucket count is tunable with -XX:StringTableSize, which matters
only if you intern aggressively.
Compact strings
Before Java 9, String wrapped a char[] — two bytes per character, regardless of content. Since
Java 9 it wraps a byte[] plus a one-byte coder flag. Text that fits in Latin-1 is stored one byte
per character; anything else falls back to UTF-16.
For a typical English-language server workload where strings are often 20–30% of live heap, this is
one of the largest single memory wins in the platform's history — commonly a 10–15% reduction in
total heap occupancy for nothing. It also means String.length() is no longer simply the array
length, and that a string containing one emoji costs twice as much as the same text without it.
Concatenation, and where the cost is
// O(n^2) in total characters copied
String out = "";
for (String line : lines) {
out += line; // new StringBuilder, new char array, new String — every iteration
}
// O(n)
StringBuilder sb = new StringBuilder(lines.size() * 32); // sized up front
for (String line : lines) {
sb.append(line);
}
String out = sb.toString();Each += compiles to a fresh concatenation of the whole accumulated string with the new piece. By
iteration n you are copying n characters, so the total work is proportional to n². With 10,000
lines of 50 characters that is roughly 2.5 billion character copies and 10,000 garbage strings.
A single expression — "user " + id + " failed after " + ms + "ms" — needs no StringBuilder. Write
it plainly. Reach for StringBuilder when the concatenation is spread across iterations or branches.
StringBuilder, StringBuffer and the rest
| Type | Mutable | Thread-safe | Use when |
|---|---|---|---|
String | No | Inherently | Values, keys, anything shared |
StringBuilder | Yes | No | Building a string in one method — the default |
StringBuffer | Yes | Yes (synchronised) | Effectively never |
StringBuffer is a Java 1.0 relic. A builder that is genuinely shared between threads is a design
mistake — build locally, publish the immutable result. Its synchronized methods are almost always
uncontended and the JIT often elides the locks, so the honest answer is "same API, synchronised, and
there is no good reason to choose it today".
Two other methods worth knowing: String.join(", ", list) for delimiting, and the
Collectors.joining(", ", "[", "]") collector for the stream form. Both are clearer than a manual
loop and avoid the trailing-separator bug.
Interview checklist
Expect the sequence: why is String immutable → what is the pool → how many objects does
new String("x") create → why == sometimes works → what is wrong with concatenating in a loop.
Each answer sets up the next, so a candidate who understands the pool tends to get all five.
The strongest single sentence to have ready: "Immutability lets the JVM cache the hash and share one
instance for every occurrence of a literal — the pool is the optimisation immutability makes safe,
and == appearing to work is a side effect of that, not a guarantee."
Frequently Asked Questions
How many objects does new String("hello") create?
Is string concatenation in a loop still slow in modern Java?
When should I use intern()?
Related tutorials
- The equals() and hashCode() ContractWhy overriding equals() without hashCode() breaks every hash-based collection, what the five contract rules guarantee, and the mutable-key bug that silently loses data.
- OOP Principles the Interviewer Actually ProbesThe OOP questions behind the textbook four: static vs dynamic dispatch, Liskov violations that compile cleanly, abstract class versus interface, and composition over inheritance.
- Exception Handling, finally and try-with-resourcesChecked versus unchecked and when each is right, how finally silently discards an exception or a return value, and suppressed exceptions in try-with-resources.
- static, final and Class Initialisation OrderThe exact order the JVM initialises a class, why a static final String can survive deleting the class that declared it, effectively final, and how two classes can deadlock while loading.