Java String Immutability
Why Java's String class is immutable — implications for security, caching, hashing, and thread safety.
Java String Immutability
A String in Java cannot be changed after it's created. Once "hello" exists, no method, no reflection trick, no clever assignment can rewrite the characters of that particular object. Every operation that "modifies" a string actually returns a new String. The class enforces this: the field that holds the bytes is private final, the class itself is final, and there is no public setter, no append, no clear.
That choice — immutability — is not a stylistic preference. It's the load-bearing decision that makes the string pool safe, hashing reliable, multithreaded sharing free, and a handful of subtle security guarantees possible at all.
What "immutable" actually means
String s = "hello";
s.toUpperCase(); // returns "HELLO" — the return value is dropped
System.out.println(s); // prints "hello"
s = s.toUpperCase(); // s now *points at* a different String
System.out.println(s); // prints "HELLO"The variable s can be reassigned — that's a property of the variable, not the object. The object originally created with "hello" is unchanged anywhere, forever, regardless of what s points at later. If another variable still references it, that variable still sees "hello".
String a = "hello";
String b = a;
a = a.toUpperCase();
System.out.println(a); // "HELLO"
System.out.println(b); // "hello" — still the originalThis is what people mean when they say strings are value-like: the contents of a String reference are as stable as the contents of an int.
Why the JVM designers chose immutability
A handful of properties fall out of immutability, and each one is worth real performance or real safety.
The string pool is safe. If "hello" could be modified in place, sharing one pooled instance across the program would be a disaster: changing it in one place would silently change it everywhere. Immutability is what makes the string pool possible at all.
hashCode() can be cached. String computes its hash on first call and stores it in a private field. That cached value would be a lie if the characters could change later, breaking every HashMap<String, ?> keyed on that string. Because the contents are stable, the cache is permanent.
Concurrent reads need no synchronization. Two threads reading the same String reference can never observe a half-modified value. There's no synchronized, no volatile, no memory-barrier dance — there's nothing that could change. Compare that to a mutable buffer, where you'd have to copy, lock, or restrict ownership.
Class loading, reflection, and security checks can trust string arguments. A ClassLoader resolves class names from Strings passed by the caller. If the string could be modified by another thread between the security check and the file open, you'd have a race-condition vulnerability — the classic time-of-check / time-of-use bug. With immutable strings, the value validated is identical to the value used.
Method arguments don't need defensive copies. When you pass a String to a method, you don't worry that it'll be mutated and surprise you on return. The receiver can store the reference directly; the caller can keep using its reference too.
The cost: mass mutation is expensive
There is a price. Building a 10,000-character string one character at a time with += allocates a brand-new String every step, copying every character it already has plus the new one. That's quadratic work — O(n²) for an O(n) task.
// Don't do this for large n
String s = "";
for (int i = 0; i < n; i++) {
s += i + ",";
}The standard library's answer is mutable buffers — StringBuilder for single-threaded code and StringBuffer for the rare shared case. They hold a resizable array, append in O(1) amortised, and produce a single immutable String at the end with toString(). That is the canonical pattern for assembling strings.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
sb.append(i).append(',');
}
String s = sb.toString();Modern JDKs optimise short, statically-shaped + chains via StringConcatFactory, so "hello, " + name + "!" is fine. The case to avoid is += inside a loop over an unknown count.
Trying to break it
Reflection can technically reach the private value field and replace it. Doing so is undefined behaviour as far as the JVM is concerned: the JIT assumes strings are immutable and will inline the cached hashCode, share references across the pool, and skip read barriers based on that promise. Mutating a String reflectively can silently corrupt unrelated code that's holding a reference to the same object. Don't. If you need mutability, you have StringBuilder for it.
Security implications
Two concrete cases where immutability matters for security:
- File paths and class names. Passed to APIs that perform an access check before opening or loading. If a path could change between check and use, sandboxes would be defeatable.
ClassLoaderkeys andStringmap keys. Stable hash codes mean an attacker can't engineer a key that "fits" in one place and silently relocates in another.
The flip side: storing passwords in a String is bad practice for the opposite reason. Once a password sits in a String, you cannot zero it out — the bytes remain in heap memory until the GC reclaims them, possibly after a heap dump has been written. For passwords, use char[] (which you can manually fill with zeros) or — better — javax.crypto.SecretKey and friends. The JDK's Console.readPassword() returns char[] precisely for this reason.
A worked example
This program creates a string, hands it to several callers, has each "mutate" it, and prints what every variable sees afterwards. The original object is visited by four references and survives unchanged. The single mutable buffer at the end is the canonical alternative when you genuinely need to build a string up.
Look at the two == comparisons. original and alias are literally the same object, so identity holds. original and upper have related contents but upper is a fresh object — there is no way for upperCase to have changed the one it was passed. That's the guarantee every Java developer leans on without thinking about it.
What's next
When you do need a string you can change, the standard library has a mutable cousin to String. It's the workhorse behind every + chain the compiler optimises, and the right answer whenever you'd otherwise reach for += in a loop. Continue to Java StringBuilder.
Practice
Which of the following is **not** a benefit of `String` being immutable in Java?