Can You Modify String Contents via Reflection?
String is designed as an immutable class. This means after creation, a string cannot be modified. But Java Reflection allows you to "look inside" any object and change even priv...
🟢 Junior Level
Technically — yes, but it’s an extremely bad idea.
String is designed as an immutable class. This means after creation, a string cannot be modified. But Java Reflection allows you to “look inside” any object and change even private and final fields.
Example (don’t do this!):
String s = "Hello";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
// Java 9+: value is byte[]
byte[] value = (byte[]) valueField.get(s);
value[0] = (byte) 'J'; // Change bytes directly
System.out.println(s); // "Jello" — string has changed!
Simple analogy: String is like a sealed bottle of water. The manufacturer expects you won’t open it. But if you unscrew the cap (reflection) and dye the water — the bottle still looks like “water”, but the content is different.
Why this is dangerous:
- All references to this string will also change — other parts of the program will see “Jello” instead of “Hello”
- String Pool will break — the literal
"Hello"in code may now mean “Jello” - Security is at risk — password hashes, keys, tokens can be altered
Conclusion: Never modify String via reflection in real code.
🟡 Middle Level
How it works
Before Java 9:
// String stored char[] value
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
char[] value = (char[]) valueField.get(s);
value[0] = 'J';
Java 9+ (Compact Strings):
// String stores byte[] value + byte coder
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
byte[] value = (byte[]) valueField.get(s);
value[0] = (byte) 'J';
The coder field determines how bytes are interpreted. If the string was Latin-1 (coder=0) and you write a byte outside the Latin-1 range — the string remains Latin-1, but data becomes corrupt.
Practical consequences
String key = "password";
String cached = key; // Another reference to the same string
// Modify via reflection
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
byte[] v = (byte[]) f.get(key);
v[0] = (byte) 'P';
System.out.println(key); // "Password"
System.out.println(cached); // "Password" — also changed!
Table of typical mistakes
| Mistake | Consequences | Solution |
|---|---|---|
| Modifying String in String Pool | All references to literal see modified value | Don’t modify literals; work only with new String(...) |
| Modifying hash field | hashCode() returns old value, HashMap.get() won’t find key |
Don’t touch hash field; if value changed — reset hash |
| Modification in multithreaded | Data race, undefined behavior | String immutable — this is the contract for all threads |
| Java 9+ module system | setAccessible(true) throws InaccessibleObjectException |
--add-opens java.base/java.lang=ALL-UNNAMED |
Approach comparison
| Method | Works? | Danger | Java 8 | Java 9+ |
|---|---|---|---|---|
Modify value[] |
Yes | Critical | ✅ | ✅ (byte[]) |
Modify coder |
Yes | Critical | N/A | ✅ |
Modify hash |
Yes | High | ✅ | ✅ |
| Create mutable String | No | N/A | ❌ | ❌ |
When NOT to use reflection on String
- Production code — never
- Libraries — never (you’ll break someone else’s code)
- Multithreaded systems — data race guaranteed
- Security-sensitive code — bypass checks, injection
- Only valid scenario: unit tests, mocking frameworks, deep serialization
🔴 Senior Level
Internal Implementation — Reflection and AccessibleObject
Reflection mechanism in JVM:
// Field.setAccessible(true) internally:
// 1. Checks module access (Java 9+)
// 2. Sets override flag in AccessibleObject
// 3. Disables access checks on get/put via JNI
Java 9+ Module System (JEP 261): With the modular system, access to internal fields is restricted:
// Without JVM flag — throws InaccessibleObjectException
Field f = String.class.getDeclaredField("value");
f.setAccessible(true); // ❌ InaccessibleObjectException
// Requires JVM flag:
// --add-opens java.base/java.lang=ALL-UNNAMED
String fields (Java 9+):
public final class String {
@Stable
private final byte[] value; // immutable after construction
private final byte coder; // 0 = LATIN1, 1 = UTF16
private int hash; // lazy: 0 = not computed yet
// serialPersistentFields, caseInsensitiveComparator, ...
}
@Stable — JVM annotation (from jdk.internal.vm.annotation) that tells the JIT compiler: the field is set once in the constructor and never changes. This enables:
- Constant folding: JIT substitutes value directly into machine code
- Register caching: value is cached in CPU register
- Escape analysis: optimizations based on immutability
Modifying @Stable field breaks JIT optimizations: compiled code may use old (cached) value.
Edge Cases (minimum 3)
1. String Pool contamination:
String s1 = "Hello"; // Literal → String Pool
String s2 = "Hello"; // Same reference from pool
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
byte[] v = (byte[]) f.get(s1);
v[0] = (byte) 'J';
System.out.println(s2); // "Jello" — literal in code now means "Jello"!
// Any new code String s = "Hello" may get the modified object
// JVM behavior is defined — literal in pool is modified. But JIT-compiled code that
// constant-folded the original value may see inconsistent results.
2. HashMap key corruption:
String key = "key";
Map<String, String> map = new HashMap<>();
map.put(key, "value");
// Modify key via reflection
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
byte[] v = (byte[]) f.get(key);
v[0] = (byte) 'K'; // "Key"
// hash field still contains hash of "key"
map.get("Key"); // null — hash doesn't match
map.get("key"); // null — such string no longer exists (value changed)
// HashMap is broken!
3. Coder mismatch — writing UTF-16 byte into Latin-1 string:
String s = "Hello"; // coder = LATIN1
Field vf = String.class.getDeclaredField("value");
Field cf = String.class.getDeclaredField("coder");
vf.setAccessible(true);
cf.setAccessible(true);
byte[] v = (byte[]) vf.get(s);
v[0] = (byte) 0xD0; // Cyrillic byte into Latin-1 string!
// s.charAt(0) returns (char) 0xD0 = 208 (invalid character)
// s.getBytes(UTF_8) returns corrupt bytes
// equals() may work unpredictably
4. Concurrent modification — data race:
// Thread A reads string
char c = s.charAt(0);
// Thread B simultaneously changes value[] via reflection
v[0] = (byte) 'X';
// Thread A: value c may be old or new — data race
// happens-before is broken: no synchronization for final fields
// modified via reflection
5. Security Manager and Reflection (Java 17+):
In Java 17+ SecurityManager is deprecated, and module access is strict by default.
Even with --add-opens some JVM-internal fields may be protected at native code level.
Performance
| Operation | Time | Note |
|---|---|---|
setAccessible(true) |
~50–200ns | One-time, but with module check more expensive |
Field.get() |
~20–50ns | JNI call overhead |
Field.set() on final field |
~20–50ns | But breaks JIT optimizations |
String.hashCode() after mutation |
Incorrect | hash field is not recalculated |
| JIT deoptimization | ~1–10μs | If JIT compiled code with constant-folded String |
JIT deoptimization: If JIT already compiled a method using this string (e.g., via constant folding "Hello" → inline), then changing the value causes deoptimization — JVM discards compiled code and reinterprets. This costs 1–10μs and may happen repeatedly.
Thread Safety
String is not thread-safe after reflection mutation. The immutable contract is broken:
- No
volatileonvalue[]— other threads may not see the change (or see it partially) - No memory barriers — happens-before is not established
@Stableannotation allows JIT to cache value in registers — thread may never see the change
Production War Story
Scenario: Testing framework (PowerMock/Mockito) used reflection to mock String in unit tests.
// Test changes string constant
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
byte[] v = (byte[]) f.get("CONSTANT");
// ... modifies value
Problem: tests passed individually, but when running in parallel (Maven Surefire, forkCount > 1) one test changed the string in String Pool, and another test failed with AssertionError. String Pool is shared across all tests in the same JVM.
Fix: Test isolation in separate JVMs (forkMode=always) or avoid mocking immutable objects.
Scenario 2: Deep serialization library (Kryo) optimized String serialization by modifying internal value[] directly. After migration to Java 17 with --add-opens not configured → InaccessibleObjectException in production. Fix: switch to standard serialization.
Monitoring
# Check module access
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
# JFR — reflection access events
java -XX:StartFlightRecording=filename=recording.jfr ...
# In JDK Flight Recorder: no special event for reflection,
# but can be caught via custom events
# GC logs — if mutation causes memory leaks
java -Xlog:gc*:file=gc.log ...
# jcmd — check flags
jcmd <pid> VM.flags | grep add-opens
// Runtime detection of reflection access
// (Java 9+: can set custom ReflectionFilter)
// Or via java.lang.instrument — intercept Field.setAccessible()
// Check field accessibility
try {
Field f = String.class.getDeclaredField("value");
f.setAccessible(true);
System.out.println("Accessible: " + f.canAccess(""));
} catch (InaccessibleObjectException e) {
System.out.println("Blocked by module system");
}
Best Practices for Highload
- Never modify String via reflection in production code
- For unit tests: use isolated JVMs or avoid mocking immutable objects
- For serialization: use standard mechanisms (Serializable, JSON, Protobuf)
- If absolutely necessary (framework development):
- Use
--add-opensonly for required modules - Don’t modify literals from String Pool
- Reset
hashfield after modifyingvalue[] - Document the requirement for all framework users
- Use
- For security-sensitive apps: set Security Manager (up to Java 17) or use JVM flags to block reflection
- Alternative:
MethodHandles.privateLookupIn()(Java 9+) — more controlled field access - For mutable strings: use
char[],byte[],StringBuilder, orByteBuffer— they’re designed for this
🎯 Interview Cheat Sheet
Must know:
- Technically can modify
byte[] valuevia reflection, but extremely dangerous - Java 9+: module system blocks
setAccessible(true)without--add-opens java.base/java.lang=ALL-UNNAMED @Stableannotation onvalue— JIT uses constant folding, modification breaks optimizations- Modifying a literal in String Pool affects ALL references to that literal across the entire app
- HashMap key corruption: modifying key via reflection →
hashCode()returns old value →get()won’t find - Only legitimate scenario: unit tests, mocking frameworks, deep serialization
Frequent follow-up questions:
- What happens when modifying a literal via reflection? — All references to that literal see the changed value.
String s = "Hello"may become"Jello". - Why is
@Stableannotation important? — JIT caches value in registers, does constant folding. Modification causes deoptimization (~1-10μs). - How does module system (Java 9+) protect from reflection? —
setAccessible(true)throwsInaccessibleObjectExceptionwithout--add-opensflag. - What happens to HashMap when key is modified? — hash field is not recalculated,
get()won’t find the key, HashMap is broken.
Red flags (DON’T say):
- ❌ “Reflection on String — normal practice” — this is an antipattern, violation of immutable contract
- ❌ “You can modify a string without consequences” — breaks String Pool, HashMap, JIT optimizations
- ❌ “
setAccessible()always works” — blocked by module system in Java 9+ - ❌ “This is thread-safe if modified in one thread” —
@Stableallows JIT to cache, other threads may see old value
Related topics:
- [[4. Why String is Immutable]]
- [[1. How String Pool Works]]
- [[19. What are Compact Strings in Java 9+]]