Question 4 · Section 12

Why String is Immutable

Every modification (concat, replace, substring) creates a new object — this generates GC pressure. In low-latency systems for hot paths, StringBuilder or byte[] directly are used.

Language versions: English Russian Ukrainian

🟢 Junior Level

Immutability means that after a String object is created, its content cannot be changed. No method can modify the characters inside a string — instead, a new string is created.

Why this matters:

  1. Security — passwords and file paths cannot be tampered with
  2. Thread safety — strings can be passed between threads without synchronization
  3. String Pool — only possible because strings cannot be changed

Example:

String s = "Hello";
s.toUpperCase();       // Creates a NEW string "HELLO"
System.out.println(s); // Prints "Hello" — original unchanged!

Simple analogy: A string is like a printed book. You can’t change the text in an already printed book — you need to print a new one instead.


🟡 Middle Level

The cost of immutability

Every modification (concat, replace, substring) creates a new object — this generates GC pressure. In low-latency systems for hot paths, StringBuilder or byte[] directly are used.

How it’s implemented in JDK

  1. final class: public final class String — cannot be inherited and methods cannot be overridden
  2. final field: private final byte[] value (Java 9+) — reference to array cannot be changed
  3. No mutators: No method like setCharAt() that changes content
  4. Defensive copying: Methods substring(), replace(), concat() return new objects

Where it’s used in practice

  • HashMap/HashSet keys: hashCode() is cached on first call and never changes
  • Security parameters: file paths, URLs, credentials — cannot be changed after verification
  • Multithreading: strings are passed between threads without any locks

Typical mistakes

  1. Mistake: Expecting replace() to change the string in place Solution: Remember the result: s = s.replace("a", "b");

  2. Mistake: Concatenation in a loop via + Solution: Use StringBuilder — each concatenation creates a new object

Comparison with alternatives

| Type | Immutable | Thread-safe | For modifications | | ————- | ———— | ————— | —————— | | String | Yes | Yes | No | | StringBuilder | No | No | Yes (single thread) | | StringBuffer | No | Yes (synchronized) | Yes (multi-thread) |


🔴 Senior Level

Internal Implementation

// OpenJDK — String structure (Java 9+)
public final class String implements Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value;  // COMPACT STRINGS: byte[] instead of char[]
    private final byte coder;    // LATIN1=0 or UTF16=1
    private int hash;            // Cached hashCode, 0 by default
    // ...
}

The @Stable annotation (JVM intrinsic) tells the JIT compiler that the value field won’t be changed after construction. This enables aggressive optimizations: constant folding, escape analysis.

Architectural Trade-offs

Why not mutable:

  1. Security: Strings are used in ClassLoader, SecurityManager, network connections, SQL queries. If strings were mutable:
    // An attacker could:
    checkAccess("/admin/config");  // Check passed
    // ... modify the string in memory ...
    readFile("/admin/config");     // Read a different file!
    
  2. String Pool: If strings were mutable, modifying "Hello" in one place would change all references to this literal throughout the application.

If strings were mutable, modifying s1 would also change s2, because they reference the same object in the pool. Immutability guarantees that one string in the pool is safe for everyone who references it.

String s1 = "Hello";
String s2 = "Hello";
// s1 and s2 — same object. If s1 could be modified, s2 would change too.
  1. HashMap stability: A mutable key in HashMap — data loss when hashCode changes.

  2. Thread safety: Immutable objects are thread-safe by design. No race conditions, no volatile, no locks.

Edge Cases

  1. Reflection: Before Java 9, you could modify value via Field.setAccessible(true). In Java 9, modular access appeared (JEP 261), and since Java 16 (JEP 396) strong encapsulation is enabled by default — --add-opens is required to access internal fields.

  2. StringBuilder under the hood: String.format(), String.join() internally use mutable buffers, but the result is always an immutable String.

  3. String concat via invokedynamic (Java 9+): StringConcatFactory generates efficient code, but the result is still immutable.

Performance

  • Allocation: New String (Java 9+, Latin1, 10 chars) ≈ 48 bytes (object + byte[])
  • GC: Short-lived string objects are efficiently handled by Young Gen (Eden → Survivor → death)
  • Cached hashCode: Computed lazily once, then O(1) access

Production Experience

Scenario: Logging in a high-load service (100K req/sec):

  • Each log string creates 3-5 temporary String objects
  • Without Compact Strings: 500MB/sec allocations → Minor GC every 200ms
  • With Compact Strings (Java 9+): 250MB/sec → Minor GC every 400ms
  • Solution: structured logging (byte[] directly into logging framework)

Monitoring

// JOL — actual String size
System.out.println(GraphLayout.parseInstance("Hello").toPrintable());
// java.lang.String object internals:
// OFFSET  SIZE  TYPE     DESCRIPTION
// 0       12             (object header)
// 12      4     byte[]   String.value
// 16      1     byte     String.coder
// ...
// Total: 24 bytes + array size

Best Practices for Highload

  • For frequent modifications: StringBuilder with pre-specified capacity
  • For inter-thread exchange: String (thread-safe without locks)
  • For parsing large texts: work with byte[]/CharBuffer directly, convert to String only when needed
  • Consider byte[] + coder pattern for ultra-low-latency systems

🎯 Interview Cheat Sheet

Must know:

  • Immutability = content cannot be changed after creation, all modifications create a new object
  • Implementation: final class, final byte[] value, @Stable annotation, no mutators
  • String Pool is only possible thanks to immutability — otherwise changing one string would affect all references
  • Thread-safe by design — no locks needed, race conditions impossible
  • Security: strings used in SecurityManager, ClassLoader, SQL queries — mutable string = vulnerability
  • HashMap keys: hashCode() is cached and never changes

Frequent follow-up questions:

  • Why is String immutable? — 4 reasons: (1) String Pool — impossible for mutable strings, (2) Thread safety — no locks, (3) Security — paths, URLs, credentials can’t be tampered with, (4) HashMap stability — hashCode doesn’t change.
  • What’s the cost of immutability? — GC pressure on modifications. Each concat/replace creates a new object.
  • Can you modify String via reflection? — Technically yes, but it breaks the contract, breaks String Pool, HashMap, JIT optimizations.
  • What to use for modifications?StringBuilder (single thread), StringBuffer (multi-thread), byte[] (low-level).

Red flags (DON’T say):

  • ❌ “replace() modifies the string in place” — returns a new string
  • ❌ “String immutable means you can’t create a new string” — you can, the original doesn’t change
  • ❌ “String is thread-safe because it’s a final class” — thread-safe because state doesn’t change
  • ❌ “Reflection is a normal way to modify String” — this is an antipattern, violates the contract

Related topics:

  • [[5. When to Use StringBuilder vs StringBuffer]]
  • [[1. How String Pool Works]]
  • [[21. Can You Modify String Contents via Reflection]]