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.
🟢 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:
- Security — passwords and file paths cannot be tampered with
- Thread safety — strings can be passed between threads without synchronization
- 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
finalclass:public final class String— cannot be inherited and methods cannot be overriddenfinalfield:private final byte[] value(Java 9+) — reference to array cannot be changed- No mutators: No method like
setCharAt()that changes content - 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
-
Mistake: Expecting
replace()to change the string in place Solution: Remember the result:s = s.replace("a", "b"); -
Mistake: Concatenation in a loop via
+Solution: UseStringBuilder— 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:
- 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! - 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.
-
HashMap stability: A mutable key in HashMap — data loss when hashCode changes.
-
Thread safety: Immutable objects are thread-safe by design. No race conditions, no
volatile, no locks.
Edge Cases
-
Reflection: Before Java 9, you could modify
valueviaField.setAccessible(true). In Java 9, modular access appeared (JEP 261), and since Java 16 (JEP 396) strong encapsulation is enabled by default —--add-opensis required to access internal fields. -
StringBuilder under the hood:
String.format(),String.join()internally use mutable buffers, but the result is always an immutable String. -
String concat via invokedynamic (Java 9+):
StringConcatFactorygenerates 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:
StringBuilderwith pre-specified capacity - For inter-thread exchange:
String(thread-safe without locks) - For parsing large texts: work with
byte[]/CharBufferdirectly, convert to String only when needed - Consider
byte[]+coderpattern for ultra-low-latency systems
🎯 Interview Cheat Sheet
Must know:
- Immutability = content cannot be changed after creation, all modifications create a new object
- Implementation:
finalclass,final byte[] value,@Stableannotation, 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
finalclass” — 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]]