Why String is Immutable
In Java, the Immutability of the String class means that once a java.lang.String object has been created in JVM memory, its internal state, length, and character contents can ne...
🟢 Junior Level
In Java, the Immutability of the String class means that once a java.lang.String object has been created in JVM memory, its internal state, length, and character contents can never be modified.
The Essence in 30 Seconds
Any operation that appears to mutate a string (concat(), replace(), substring(), toUpperCase()) does not change the original instance. Instead, it always allocates and returns a brand new String object on the heap with the updated contents!
String original = "hello";
// An attempt to "mutate" the string:
original.toUpperCase();
// The original string remains completely unaltered:
System.out.println(original); // Prints: "hello"
// To preserve the result, assign the reference to the newly returned object:
String upper = original.toUpperCase();
System.out.println(upper); // Prints: "HELLO"
The Four Fundamental Reasons Why String is Immutable
- String Pool Optimization: Sharing identical string literals throughout the application saves megabytes of RAM. If strings were mutable, changing a literal (
s1 = "Hello") in one component would corrupt the state of all other components referencing that same pooled literal. - Security & Parameter Integrity: Strings represent database credentials, network URLs, filesystem paths, and class names loaded by
ClassLoader. Immutability guarantees that once these values are validated, they cannot be altered maliciously by concurrent threads. - Thread Safety by Design: Immutable objects are inherently thread-safe without locks or synchronization. A string can be safely read concurrently by hundreds of worker threads without race conditions.
- Cached Hash Code Performance: Because the characters can never change,
hashCode()is computed once upon first access and permanently cached. This makesStringthe fastest and most reliable key for hash tables (HashMap,HashSet).
🟡 Middle Level
How Immutability is Enforced in the JDK
In the OpenJDK source code, immutability is enforced through a strict combination of language primitives:
// java.lang.String (OpenJDK 17/21)
public final class String implements Serializable, Comparable<String>, CharSequence {
@Stable
private final byte[] value; // 1. Private and final byte array
private final byte coder; // 2. Encoding format (LATIN1 or UTF16)
private int hash; // 3. Cached 32-bit hash code
private boolean hashIsZero; // 4. Collision flag for strings with hash == 0
// No mutators: no setCharAt(), no append(), no in-place updates.
}
finalClass: Prevents malicious subclasses (extends String) from overriding methods or introducing mutable state.private final byte[] value: The reference to the backing byte array cannot be reassigned after construction, andprivateprevents external direct access.- Absence of Mutators: Every single mutating method operates via Defensive Copying, allocating a fresh byte array and returning a new
Stringinstance.
The Trade-Off: High Garbage Allocation Rate
The primary cost of immutability is frequent object allocation during intensive string manipulation loops:
// SEVERE ANTI-PATTERN: massive transient heap allocations:
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i; // Allocates a new String and byte[] on every single iteration!
}
This loop allocates ~10,000 intermediate string objects, rapidly saturating the Young Generation (Eden Space) and triggering continuous GC pauses.
For bulk string assembly, developers must use mutable buffers: StringBuilder (for single-threaded execution) or StringBuffer (for legacy synchronized contexts).
🔴 Senior Level
TOCTOU (Time-of-Check to Time-of-Use) Vulnerability Mitigation
A catastrophic security vulnerability would exist across the JVM if strings were mutable:
// Vulnerable architectural scenario IF String were mutable:
public void openSecureFile(String path) {
// 1. Time-of-Check: Security validation passes
if (!securityManager.isAccessAllowed(path)) { // path = "/public/reports/sales.csv"
throw new SecurityException("Access Denied");
}
// <-- AT THIS EXACT MOMENT, a concurrent thread mutates: path.setValue("/etc/shadow")!
// 2. Time-of-Use: The OS file descriptor opens
fileSystem.open(path); // System file is opened, completely bypassing the security check!
}
Because String is immutable, the path reference is guaranteed to point to unchanging characters, making TOCTOU state-tampering attacks physically impossible across security boundaries and classloaders.
The @Stable Annotation & JIT C2 Compiler Optimizations
Notice the @Stable annotation applied to private final byte[] value:
- In standard Java, a
finalarray reference prevents reassigning the array pointer, but the array’s elements remain technically mutable in JVM memory. - The HotSpot-internal
@Stableannotation instructs the JIT Compiler (C2):“The elements of this array will never change after construction.”
- This unlocks powerful low-level optimizations:
- Machine-Code Constant Folding: Expressions like
str.charAt(0)are folded directly into CPU register constants. - SIMD Vectorization: String searches and comparisons are converted to hardware vector instructions (AVX-2 / AVX-512).
- Bounds Check Elimination: Array bounds checks inside string loops are eliminated from compiled assembly code.
- Machine-Code Constant Folding: Expressions like
Why Passwords and Secrets Must NEVER Be Stored in String
Despite its security strengths, storing passwords, cryptographic keys, or authorization tokens in String is a critical security vulnerability:
Stringis immutable: application code cannot zero out (wipe) its memory after authentication completes.- The password remains in the Java Heap until the Garbage Collector sweeps it (which may take hours or days if promoted to Old Generation).
- In the event of a Core Dump, JVM heap dump, or memory inspection exploit, plaintext secrets are readily accessible.
- Senior Solution: Passwords must be stored in mutable arrays:
char[]orbyte[]. Immediately after authentication, the memory must be sanitized explicitly viaArrays.fill(password, '\0').
4 Tricky Interview Questions
1. How does hash code caching work in String, and why was the hashIsZero boolean field introduced in Java 13?
Answer:
The string hash code is computed lazily upon the first invocation of hashCode() and stored in private int hash. Subsequent calls return the cached integer directly.
The Zero-Hash Degeneration Trap:
Prior to Java 13, the lookup logic was: if (hash == 0 && value.length > 0) { hash = computeHash(); }.
If a string happened to produce a computed hash of exactly 0 (e.g., "DA", "DB", or specific character sequences), the condition hash == 0 was met on every single invocation, causing the JVM to recalculate the hash from scratch across the entire character array every time! Java 13 introduced private boolean hashIsZero; to indicate that a hash of zero is the valid, already computed value, preventing redundant recalculation loops.
2. What happens in modern Java (Java 17/21+) if code attempts to modify the value array of a String via Java Reflection?
Answer:
The JVM throws a java.lang.reflect.InaccessibleObjectException.
Starting with Java 16 and 17 under JEP 396 and JEP 403 (Strong Encapsulation of JDK Internals), reflective access to internal private fields of the java.base module is strictly disallowed by default. Calling field.setAccessible(true) on String.value immediately fails unless the JVM is started with the explicit command-line override: --add-opens java.base/java.lang=ALL-UNNAMED.
3. Why is StringBuilder preferred over StringBuffer, and under what rare circumstances would StringBuffer still be used?
Answer:
StringBuilder(introduced in Java 5) is completely unsynchronized, making it significantly faster by avoiding monitor lock acquisition (synchronized) and memory barrier overhead. In 99.9% of application scenarios, string concatenation is confined to a single thread within a method body.StringBufferis a legacy thread-safe class whose methods are decorated withsynchronized. It is only used when a single mutable character buffer is concurrently modified across multiple threads without external synchronization.
4. Does the Java Memory Model (JMM) guarantee safe publication of String across threads without synchronization?
Answer:
Yes, absolutely.
Under the Java Memory Model (JLS §17.5 Final Field Semantics), fields marked with final carry explicit memory visibility guarantees upon object creation:
- When a
Stringconstructor completes execution, hardware memory barriers (StoreStore / LoadStore) ensure that all writes tofinal byte[] valueandfinal byte coderare made globally visible to all other CPU cores before theStringreference can be read. - No thread in a concurrent environment can ever observe a partially initialized string or a
nullreference forvalue, even when published withoutvolatileorsynchronizedprimitives.
🎯 Interview Cheat Sheet
30-Second Summary
“
Stringis immutable because its internal state cannot be modified once instantiated; all mutation methods return newly allocated objects. Immutability is enforced viapublic final class,private final byte[] value, the@Stableintrinsic, and defensive copying. It provides four fundamental benefits: enables the String Pool for memory savings, guarantees thread safety by design (JLS §17.5), blocks TOCTOU security exploits on file/network paths, and enables hash code caching for $O(1)$HashMaplookups. However, secrets and passwords should never be stored inString; usechar[]instead to allow immediate memory wiping.”
Key Concepts Checklist
- Enforcement:
finalclass,private final byte[] value,@Stableannotation, zero mutators. - Core Reasons: String Pool reuse, security against TOCTOU attacks, lock-free concurrency, cached
hashCode(). - Secret Storage Warning: Never store passwords in
String; usechar[]and clean withArrays.fill(arr, '\0'). - Reflection Defense: Modern Java (17/21+) blocks reflective mutations via JEP 403 (
InaccessibleObjectException). - JMM Guarantees: JLS §17.5 final field semantics guarantee safe cross-thread visibility without locks.
Common Pitfalls & Red Flags
- ❌ “Methods like
replace()orsubstring()mutate the string in-place.” (False: they always allocate and return a newStringobject). - ❌ “String is thread-safe because its internal methods are synchronized.” (False:
Stringhas zero synchronized methods; thread safety comes from immutability). - ❌ “Storing passwords in String is secure because Strings cannot be modified.” (False: passwords cannot be cleared from memory, exposing them in heap dumps).
Related Topics
- When to Use StringBuilder vs StringBuffer — Mutable alternatives
- How String Pool Works — Pool architecture
- What are Compact Strings in Java 9+ —
byte[] valueand coder byte - Can You Modify String Contents via Reflection — Module encapsulation limits