Strings, StringBuilder & the String Pool
Why String is immutable, how the string pool and interning work, == vs equals, and efficient string building.
String is immutable
Every “modification” of a String — concat, replace, toUpperCase — returns a new object. The original never changes.
String s = "hello";
s.toUpperCase(); // result discarded
System.out.println(s); // "hello"
s = s.toUpperCase(); // reassign the reference to the new object
Why immutability?
- String pool — identical literals can be safely shared
- Security — file paths, URLs, class names and DB credentials can’t be changed after validation
- Thread safety — immutable objects need no synchronisation
- Hash caching —
hashCode()is computed once, making strings excellentHashMapkeys
The string pool
String literals are stored in a pool (in the heap since Java 7). The JVM reuses the same object for identical literals; new String(...) always creates a new object.
class Main {
public static void main(String[] args) {
String a = "java";
String b = "java";
String c = new String("java");
String d = c.intern();
String e = "ja" + "va"; // compile-time constant -> folded into "java"
String part = "ja";
String f = part + "va"; // computed at runtime -> new object
System.out.println("a == b " + (a == b)); // same pooled literal
System.out.println("a == c " + (a == c)); // c is a new heap object
System.out.println("a.equals(c) " + a.equals(c)); // same characters
System.out.println("a == d " + (a == d)); // intern() returns the pooled one
System.out.println("a == e " + (a == e));
System.out.println("a == f " + (a == f));
System.out.println("a == f.intern " + (a == f.intern()));
}
}Concatenation performance
In a loop, += creates a new string every iteration — O(n²) copying:
String csv = "";
for (String s : items) csv += s + ","; // slow for large lists
Use StringBuilder (not thread-safe, fast) — or better, a higher-level API:
var sb = new StringBuilder();
for (String s : items) sb.append(s).append(',');
String joined = String.join(",", items);
String joined2 = items.stream().collect(Collectors.joining(", ", "[", "]"));
String |
StringBuilder |
StringBuffer |
|
|---|---|---|---|
| Mutable | No | Yes | Yes |
| Thread-safe | Yes (immutable) | No | Yes (synchronised) |
| Use when | Values, keys, constants | Building strings in one thread | Legacy code — rarely needed |
Useful modern String APIs
" hi ".strip(); // "hi" — Unicode-aware (Java 11), prefer over trim()
"".isBlank(); // true (Java 11)
"ab".repeat(3); // "ababab" (Java 11)
"a\nb\nc".lines().count(); // 3 (Java 11)
"Hello %s".formatted("Ada"); // "Hello Ada" (Java 15)
"ABC".chars().sum(); // IntStream of code units
"hello".indent(4); // adds 4 spaces to each line (Java 12)
Text blocks (Java 15) make multi-line strings readable:
String json = """
{
"name": "Ada",
"role": "admin"
}
""";
Compact strings
Since Java 9, a String stores its characters in a byte[] with a coder flag: Latin-1 strings use one byte per character instead of two, roughly halving memory for typical English text. This is invisible to your code.
Mark it complete to track your progress through Modern Java.