🔤 Section 12 · Question #4

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

  1. 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.
  2. 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.
  3. 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.
  4. Cached Hash Code Performance: Because the characters can never change, hashCode() is computed once upon first access and permanently cached. This makes String the 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.
}
  1. final Class: Prevents malicious subclasses (extends String) from overriding methods or introducing mutable state.
  2. private final byte[] value: The reference to the backing byte array cannot be reassigned after construction, and private prevents external direct access.
  3. Absence of Mutators: Every single mutating method operates via Defensive Copying, allocating a fresh byte array and returning a new String instance.

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 final array reference prevents reassigning the array pointer, but the array’s elements remain technically mutable in JVM memory.
  • The HotSpot-internal @Stable annotation 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.

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:

  1. String is immutable: application code cannot zero out (wipe) its memory after authentication completes.
  2. The password remains in the Java Heap until the Garbage Collector sweeps it (which may take hours or days if promoted to Old Generation).
  3. In the event of a Core Dump, JVM heap dump, or memory inspection exploit, plaintext secrets are readily accessible.
  4. Senior Solution: Passwords must be stored in mutable arrays: char[] or byte[]. Immediately after authentication, the memory must be sanitized explicitly via Arrays.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.
  • StringBuffer is a legacy thread-safe class whose methods are decorated with synchronized. 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 String constructor completes execution, hardware memory barriers (StoreStore / LoadStore) ensure that all writes to final byte[] value and final byte coder are made globally visible to all other CPU cores before the String reference can be read.
  • No thread in a concurrent environment can ever observe a partially initialized string or a null reference for value, even when published without volatile or synchronized primitives.

🎯 Interview Cheat Sheet

30-Second Summary

“String is immutable because its internal state cannot be modified once instantiated; all mutation methods return newly allocated objects. Immutability is enforced via public final class, private final byte[] value, the @Stable intrinsic, 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)$ HashMap lookups. However, secrets and passwords should never be stored in String; use char[] instead to allow immediate memory wiping.”

Key Concepts Checklist

  1. Enforcement: final class, private final byte[] value, @Stable annotation, zero mutators.
  2. Core Reasons: String Pool reuse, security against TOCTOU attacks, lock-free concurrency, cached hashCode().
  3. Secret Storage Warning: Never store passwords in String; use char[] and clean with Arrays.fill(arr, '\0').
  4. Reflection Defense: Modern Java (17/21+) blocks reflective mutations via JEP 403 (InaccessibleObjectException).
  5. JMM Guarantees: JLS §17.5 final field semantics guarantee safe cross-thread visibility without locks.

Common Pitfalls & Red Flags

  • ❌ “Methods like replace() or substring() mutate the string in-place.” (False: they always allocate and return a new String object).
  • ❌ “String is thread-safe because its internal methods are synchronized.” (False: String has 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).