Strings, the string pool, and StringBuilder
Why String is immutable, when == happens to work on strings, and why concatenating in a loop is the classic hidden O(n²).
After this lesson you can
- Explain what the string pool is and why == on strings is unreliable
- Say why String is immutable and what that buys
- Build strings in a loop with StringBuilder instead of +=
- Explain why length() can disagree with the number of characters a reader sees
A String is immutable: no method changes it. toUpperCase, trim,
replace and concat all return a new string, and the original is
untouched — so name.trim(); on its own line does nothing useful.
Immutability is what makes the rest of the design safe. A string can be
shared between threads without locking, used as a HashMap key without
its hash code ever changing (String caches it after the first call),
and handed to a security check without the caller changing it
afterwards.
The string pool, and why == is a trap
String literals are interned: every occurrence of the literal
"java" in a program refers to one shared object in the string pool.
new String("java") deliberately makes a separate object, and a string
built at runtime (by concatenating variables, reading input, calling
substring) is a new object too. Concatenation of two literals,
though, is done by the compiler, so "ja" + "va" is the pooled
"java".
import java.util.*; public class Solution { public static List<String> describe() { String literal = "java"; String other = "java"; String made = new String("java"); String ja = "ja"; String built = ja + "va"; // built at runtime String folded = "ja" + "va"; // folded by the compiler return List.of( "literal == other: " + (literal == other), "literal == made: " + (literal == made), "literal.equals(made): " + literal.equals(made), "literal == built: " + (literal == built), "literal == folded: " + (literal == folded), "literal == built.intern(): " + (literal == built.intern())); }}== compares references, so it is true exactly when two expressions
happen to reach the same object — which depends on how each string was
produced, not on its text. equals compares the characters, always.
intern() returns the pooled copy, which is why it makes == work
again; it is not a reason to use ==.
StringBuilder
Because each += on a String creates a new string and copies every
character so far into it, building an n-character result one piece at
a time costs O(n²) copying. StringBuilder keeps one growable buffer
and appends into it, and has the editing operations String lacks.
import java.util.*; public class Solution { public static String describe() { StringBuilder sb = new StringBuilder(); for (int i = 1; i <= 5; i++) { sb.append(i).append(','); } sb.setLength(sb.length() - 1); // drop the trailing comma String joined = sb.toString(); sb.insert(0, '[').append(']'); String wrapped = sb.toString(); StringBuilder word = new StringBuilder("color"); word.insert(4, 'u').replace(0, 1, "C"); // edits in the middle and at the front return joined + " | " + wrapped + " | " + word; }}StringBuilder is not thread-safe; StringBuffer is the old
synchronized twin, and is almost never what you want today. A single
expression such as a + ":" + b is fine — the compiler already turns
it into one efficient concatenation; the cost is in the loop.
length() counts UTF-16 units
A String is a sequence of UTF-16 chars. Characters outside the
Basic Multilingual Plane — most emoji, for example — take two
chars (a surrogate pair). length() counts chars;
codePointCount counts the characters a reader sees.
Common mistakes
- Comparing strings with
==. It passes for literals (the pool) and fails for anything read or built at runtime. - Ignoring the return value:
s.toLowerCase();changes nothing. split(".")orsplit("|"). The argument is a regular expression:"."matches every character, so splitting"a.b.c"on it yields an empty array. Escape it:split("\\.").- in a loop over thousands of pieces, where a (or / ) does the same in linear time.
import java.util.*; public class Solution { public static List<String> describe() { String version = "21.0.2"; String[] wrong = version.split("."); String[] right = version.split("\\."); String word = "café😀"; // "café" followed by an emoji return List.of( "split(\".\") parts: " + wrong.length, "split(\"\\\\.\") parts: " + Arrays.toString(right), "length: " + word.length(), "codePointCount: " + word.codePointCount(0, word.length())); }}What interviewers ask
- Why is
Stringimmutable in Java? Safe sharing via the pool, thread safety without locks, a hash code that can be cached and never changes, and values that cannot be altered after a security check. String,StringBuilder,StringBuffer— which and when?Stringfor values;StringBuilderfor building in a loop on one thread;StringBufferonly for legacy APIs that require it.- How many objects does
new String("hi")create? One newStringon the heap each time it runs, plus the pooled"hi"literal, which exists once per JVM.
Exercise · Pro
camelCase to snake_case with a StringBuilder
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade