Java Immutable Strings
Java Immutable Strings
In Java, once you create a String, its content cannot be changed. Not a single character. Not the length. Nothing. This is what immutability means for Strings ā the object is fixed the moment it is created.
This sounds like a restriction. It is actually one of the most useful design decisions in the Java language. Immutability is what makes the String Pool possible, what makes Strings safe to share between threads without locks, what makes them reliable as HashMap keys, and what prevents a category of security vulnerabilities where someone could change a value after validation. Every senior Java developer understands why String is immutable ā and every Java interview asks about it.
What Immutability Means for Strings
When a String is created, its characters are stored in a final char[] (or byte[] in Java 9+) inside the object. That array cannot be replaced, resized, or modified. There is no setCharAt() method on String. There is no way to swap characters. The object is sealed at creation.
String creation and immutability:
String name = "Priya";
Memory:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā String object ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā value (char[]): ['P','r','i','y','a']ā ā ā sealed ā cannot change
ā ā hash (int) : 0 (computed lazily) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā No setter for 'value' exists ā
ā No method can replace or modify this array ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
What "name = name.toUpperCase()" ACTUALLY does:
Step 1: name.toUpperCase() creates a NEW String: "PRIYA"
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā new String object: "PRIYA" ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Step 2: name variable now points to the new String
name āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāŗ "PRIYA" (new object)
Old "Priya" object āāāāāāāāāāāāāāāāāāŗ still exists until GC
Proof of Immutability ā Methods Return New Strings
Every method on String that appears to modify text actually creates and returns a new String. The original object is completely untouched.
// File: ImmutabilityProof.java
public class ImmutabilityProof {
public static void main(String[] args) {
String original = " hello, world! ";
System.out.println("=== Every method returns a NEW String ===");
System.out.println("Original : '" + original + "'");
// None of these modify 'original' ā each returns a new object
String trimmed = original.trim();
String upper = original.toUpperCase();
String replaced = original.replace("world", "Java");
String substring = original.substring(2, 7);
System.out.println("After trim() : '" + trimmed + "'");
System.out.println("After upper() : '" + upper + "'");
System.out.println("After replace() : '" + replaced + "'");
System.out.println("After substring : '" + substring + "'");
System.out.println();
System.out.println("Original STILL : '" + original + "'"); // unchanged
System.out.println();
// Identity hash codes prove they are different objects
System.out.println("=== Different Objects ===");
System.out.println("original id : " + System.identityHashCode(original));
System.out.println("trimmed id : " + System.identityHashCode(trimmed)); // different
System.out.println("upper id : " + System.identityHashCode(upper)); // different
System.out.println("replaced id : " + System.identityHashCode(replaced)); // different
System.out.println();
// The most common beginner mistake ā calling method without capturing result
System.out.println("=== The Most Common Mistake ===");
String username = " rohan_mehta ";
username.trim(); // result thrown away ā username unchanged
username.toUpperCase(); // result thrown away ā username unchanged
System.out.println("Without capture: '" + username + "'"); // still padded
// Correct ā always capture the return value
username = username.trim().toUpperCase();
System.out.println("With capture : '" + username + "'"); // correct
}
}Output:
=== Every method returns a NEW String ===
Original : ' hello, world! '
After trim() : 'hello, world!'
After upper() : ' HELLO, WORLD! '
After replace() : ' hello, Java! '
After substring : 'hello'
Original STILL : ' hello, world! '
=== Different Objects ===
original id : 1173230247
trimmed id : 856419764
upper id : 2018699554
replaced id : 1311053135
=== The Most Common Mistake ===
Without capture: ' rohan_mehta '
With capture : 'ROHAN_MEHTA'
Four different identity hash codes confirm four different objects in memory. The original is always at address 1173230247 ā none of the method calls touched it.
Why Java Made String Immutable ā Five Reasons
Reason 1 ā The String Pool
If strings were mutable, sharing them in a pool would be catastrophically unsafe. Two variables pointing to the same pooled String ā if one could change the characters, the other would silently see different content.
// File: PoolSafetyDemo.java
public class PoolSafetyDemo {
public static void main(String[] args) {
// Both point to the SAME pool entry
String city1 = "Delhi";
String city2 = "Delhi";
System.out.println("city1 == city2 : " + (city1 == city2)); // true ā same object
// If Strings were mutable (hypothetical ā this does NOT work in Java):
// city1.setCharAt(0, 'B'); // would change "Delhi" to "Belhi"
// System.out.println(city2); // would print "Belhi" ā silent corruption!
// Because String is immutable:
city1 = city1.replace("Delhi", "Mumbai"); // creates NEW String ā pool entry unchanged
System.out.println("city1 : " + city1); // Mumbai
System.out.println("city2 : " + city2); // Delhi ā still safe in the pool
}
}Output:
city1 == city2 : true
city1 : Mumbai
city2 : Delhi
Reason 2 ā Thread Safety Without Locks
Immutable objects can be shared across threads freely ā no synchronisation needed. Since no thread can change a String's content, concurrent reads are always safe.
// File: ThreadSafetyDemo.java
public class ThreadSafetyDemo {
// This String is shared by multiple threads
private static final String CONFIG_URL = "https://api.meesho.com/v2";
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// All threads read the same String ā completely safe
// No thread can modify CONFIG_URL ā it is immutable
String processed = CONFIG_URL.toUpperCase(); // creates a NEW String per thread
System.out.println(Thread.currentThread().getName()
+ " ā URL: " + CONFIG_URL
+ " | Upper: " + processed);
};
Thread[] threads = new Thread[4];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(task, "Worker-" + (i + 1));
}
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
// CONFIG_URL is exactly what it was ā no thread could have changed it
System.out.println("\nCONFIG_URL intact: " + CONFIG_URL);
}
}Output:
Worker-1 ā URL: https://api.meesho.com/v2 | Upper: HTTPS://API.MEESHO.COM/V2
Worker-3 ā URL: https://api.meesho.com/v2 | Upper: HTTPS://API.MEESHO.COM/V2
Worker-2 ā URL: https://api.meesho.com/v2 | Upper: HTTPS://API.MEESHO.COM/V2
Worker-4 ā URL: https://api.meesho.com/v2 | Upper: HTTPS://API.MEESHO.COM/V2
CONFIG_URL intact: https://api.meesho.com/v2
Reason 3 ā Safe HashMap Keys
HashMap and HashSet rely on a key's hashCode() never changing after insertion. If a String used as a key could be mutated, the hash would change, and HashMap.get() would look in the wrong bucket and return null ā even though the entry still exists.
// File: HashMapKeyDemo.java
import java.util.HashMap;
public class HashMapKeyDemo {
public static void main(String[] args) {
// String as HashMap key ā always safe because String is immutable
HashMap<String, String> userRoles = new HashMap<>();
String userId = "USR-001";
userRoles.put(userId, "ADMIN");
// Even if we "change" userId, the original String is untouched
// userId = "USR-002" only changes the VARIABLE, not the String in the map
userId = "USR-002"; // variable now points to a different String
// The map still has USR-001 because String is immutable
System.out.println("Get USR-001: " + userRoles.get("USR-001")); // ADMIN
System.out.println("Get USR-002: " + userRoles.get("USR-002")); // null
// Why this matters ā if String were mutable like StringBuilder:
// The hashCode of the key would change, making the entry unfindable
// Immutability guarantees the hash never changes after insertion
System.out.println();
System.out.println("Map is safe because String hashCode never changes.");
System.out.println("String 'USR-001' hashCode: " + "USR-001".hashCode());
System.out.println("Same value, same hash always: " +
("USR-001".hashCode() == "USR-001".hashCode())); // always true
}
}Output:
Get USR-001: ADMIN
Get USR-002: null
Map is safe because String hashCode never changes.
String 'USR-001' hashCode: 1426657780
Same value, same hash always: true
Reason 4 ā Security
Many security-critical operations use Strings ā file paths, passwords, network addresses, class names for reflection. If Strings were mutable, a thread could change the value after it was validated but before it was used ā a classic time-of-check to time-of-use (TOCTOU) vulnerability.
// File: SecurityDemo.java
public class SecurityDemo {
// Validates and opens a file
public static void openSecureFile(String filePath) {
// Step 1 ā validate
if (!filePath.startsWith("/secure/") || filePath.contains("..")) {
throw new SecurityException("Access denied: " + filePath);
}
// If String were mutable, another thread could change filePath
// between the validation above and the use below
// e.g., change "/secure/report.pdf" to "/etc/passwd"
// Step 2 ā use ā safe because String is immutable
System.out.println("Opening file: " + filePath);
// actual file operation would go here
}
public static void main(String[] args) {
// Safe ā filePath cannot be changed between validation and use
String path = "/secure/reports/q4-sales.pdf";
openSecureFile(path);
// This creates a NEW String ā the old validated path is untouched
path = "/secure/reports/q3-sales.pdf";
openSecureFile(path);
System.out.println();
System.out.println("Security benefit: validated value cannot be changed");
System.out.println("after validation by any other code or thread.");
}
}Output:
Opening file: /secure/reports/q4-sales.pdf
Opening file: /secure/reports/q3-sales.pdf
Security benefit: validated value cannot be changed
after validation by any other code or thread.
Reason 5 ā Cached HashCode
String caches its hashCode() after the first computation. This is safe only because the content never changes ā the cached value is always valid. Every subsequent call to hashCode() on the same String object returns the cached value instantly, with zero recomputation.
// File: HashCodeCacheDemo.java
public class HashCodeCacheDemo {
public static void main(String[] args) {
String text = "DevStackFlow";
// First call ā computes and caches the hash
long start1 = System.nanoTime();
int hash1 = text.hashCode();
long time1 = System.nanoTime() - start1;
// Subsequent calls ā returns cached value immediately
long start2 = System.nanoTime();
int hash2 = text.hashCode();
long time2 = System.nanoTime() - start2;
System.out.println("hash1 : " + hash1);
System.out.println("hash2 : " + hash2);
System.out.println("Same : " + (hash1 == hash2)); // always true
System.out.println();
System.out.println("This caching is safe only because the String cannot change.");
System.out.println("If characters could be modified, the cached hash would be wrong.");
}
}Output:
hash1 : -1285326154
hash2 : -1285326154
Same : true
This caching is safe only because the String cannot change.
If characters could be modified, the cached hash would be wrong.
Why String Is Declared final
The String class is declared final in Java:
public final class String implements ... { ... }final on a class prevents subclassing. Without it, a developer could create a MutableString extends String that overrides methods to allow modification ā breaking every assumption that libraries, frameworks, and the JVM make about String immutability. Making the class final closes this loophole entirely.
// File: FinalClassDemo.java
public class FinalClassDemo {
public static void main(String[] args) {
// You cannot extend String
// class CustomString extends String { } // compile error
// You cannot create a mutable subtype that disguises as String
// This is intentional ā it protects all code that trusts String immutability
// What final on String guarantees:
// 1. String's behaviour cannot be overridden by subclasses
// 2. Every String IS-A String ā no tricks, no surprises
// 3. Libraries can trust that any String parameter behaves as documented
System.out.println("String is final ā cannot be subclassed.");
System.out.println("This protects immutability at the language level.");
// Demonstrate final field inside String (conceptually)
// The internal char[] value is private final ā cannot be replaced
String s = "hello";
System.out.println("String s: " + s);
// s.value = new char[]{'w','o','r','l','d'}; // not possible ā private final
System.out.println("Content is sealed at: " + System.identityHashCode(s));
}
}Output:
String is final ā cannot be subclassed.
This protects immutability at the language level.
String s: hello
Content is sealed at: 1173230247
Immutability vs Reassignment ā The Key Distinction
This is the most important nuance: the variable can change; the String object cannot.
// File: VariableVsObjectDemo.java
public class VariableVsObjectDemo {
public static void main(String[] args) {
String message = "Hello";
System.out.println("message id: " + System.identityHashCode(message));
// Reassignment ā the VARIABLE changes, the original OBJECT does not
message = "Hello" + ", World"; // creates a NEW String
System.out.println("message id: " + System.identityHashCode(message)); // different id
// The String "Hello" still exists in the pool ā it was not modified
String hello = "Hello"; // retrieves the SAME pooled "Hello" from before
System.out.println("'Hello' id: " + System.identityHashCode(hello));
System.out.println();
// What changes: What does NOT change:
// āāāāāāāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// Which object the The content of any String object
// variable points to The characters in the char[] array
// The hashCode of any String object
// The String Pool entries
// Practical impact:
String url = "https://api.razorpay.com";
processUrl(url);
System.out.println("After method call: " + url); // unchanged ā method cannot modify it
}
static void processUrl(String u) {
// u = u.replace("razorpay", "malicious"); // only changes local variable u
// caller's 'url' is completely unaffected
System.out.println("Processing: " + u.toUpperCase()); // returns new String
}
}Output:
message id: 1173230247
message id: 856419764
'Hello' id: 1173230247
Processing: HTTPS://API.RAZORPAY.COM
After method call: https://api.razorpay.com
"Hello" and "Hello" resolve to the same pool entry ā same identity hash code. The variable message changed to point to a new object, but the "Hello" pool entry was never touched.
Immutability ā Benefits vs Tradeoffs Comparison Table
| Aspect | Benefit of Immutability | Tradeoff |
|---|---|---|
| String Pool | Safe sharing ā multiple variables point to one object | Cannot modify shared entry |
| Thread safety | No synchronisation needed for concurrent reads | Cannot update a shared String value in place |
| HashMap keys | Stable hashCode ā reliable lookups always | Cannot use as a key when content must evolve |
| Security | Validated value cannot be changed after validation | Workarounds needed for secure mutable text (char[]) |
| HashCode caching | Computed once, reused forever ā fast repeated hashing | Slight memory overhead to store cached value |
| Subclassing | final class prevents subtype attack | Cannot extend String for custom behaviour |
| Memory in loops | Predictable ā every concat creates a known new object | More GC pressure if building strings iteratively |
| Method return safety | Cannot leak internal state ā every method returns a copy | Callers must always capture return values |
Basic Example
public class Main {
public static void main(String[] args) {
String greeting = "Hello";
System.out.println("Before: " + greeting);
// toUpperCase() does NOT change greeting ā it returns a NEW String
String upper = greeting.toUpperCase();
System.out.println("Original after call: " + greeting); // unchanged
System.out.println("New string returned : " + upper);
System.out.println("Same object? " + (greeting == upper)); // false
}
}Output
Before: Hello Original after call: Hello New string returned : HELLO Same object? false
How it works:
- āŗ
greetingis created once and holds the text"Hello". - āŗCalling
greeting.toUpperCase()does not modifygreetingā String has no method that can change its own characters. - āŗInstead,
toUpperCase()builds a brand newStringobject,"HELLO", and returns it. - āŗ
greeting == upperisfalse, proving they are two separate objects in memory ā the original was never touched.
Best Practices
Declare String constants as static final. Application-wide values ā base URLs, error codes, configuration keys, status codes ā should be public static final String. They are loaded once at class initialisation, stored in the pool, and shared safely by every part of the application without copying.
Always capture the return value of String methods. str.trim() does nothing useful unless you write str = str.trim() or String clean = str.trim(). The most common String-related bug in beginner code is calling a method and discarding its result. Every method on String returns a new object ā if you do not capture it, the operation has no effect.
Use char[] instead of String for sensitive data like passwords. String is immutable ā you cannot clear its content after use. A password stored in a String stays in memory until garbage collected, and may even be in the String Pool indefinitely. A char[] can be explicitly zeroed: Arrays.fill(passwordChars, '\0'). This is why JPasswordField.getPassword() returns char[], not String.
Use StringBuilder when building strings iteratively. Immutability means every + in a loop creates a new String. For 1,000 iterations, that is 1,000 discarded objects. StringBuilder uses a single mutable buffer throughout and creates one String at the end. Immutability makes String correct for values; StringBuilder makes it efficient for building.
Common Mistakes
Mistake 1 ā Assuming a Method Modified the String
String phone = " 98765-43210 ";
phone.trim(); // returns trimmed String ā but it is thrown away
phone.replace("-", ""); // returns cleaned String ā also thrown away
System.out.println(phone); // " 98765-43210 " ā still unchanged
// Fix ā capture every return value
phone = phone.trim().replace("-", "");
System.out.println(phone); // "9876543210"Mistake 2 ā Storing Passwords as String
// BAD ā String stays in memory, in pool, cannot be cleared
String password = getPasswordFromUser(); // might be pooled
verifyPassword(password);
password = null; // does not wipe the String from memory
// GOOD ā char[] can be explicitly cleared
char[] passwordChars = getPasswordCharsFromUser();
verifyPassword(new String(passwordChars)); // convert only when needed
java.util.Arrays.fill(passwordChars, '\0'); // zero out ā sensitive data goneMistake 3 ā Using += in a Loop
// Immutability makes this O(n²) ā creates a new String every iteration
String output = "";
for (int i = 0; i < 10000; i++) {
output += "item" + i + "\n"; // 10,000 String objects created and discarded
}
// Fix ā use StringBuilder, call toString() once
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("item").append(i).append("\n");
}
String output = sb.toString(); // one String created at the endMistake 4 ā Confusing Variable Reassignment With Object Mutation
void process(String value) {
value = value.trim(); // only changes local parameter ā caller unchanged
System.out.println("Inside: " + value); // trimmed
}
String data = " hello ";
process(data);
System.out.println("Outside: " + data); // still " hello " ā method did not mutate itA method that reassigns its String parameter only changes the local variable. The caller's reference still points to the original. Immutability reinforces this ā even if the local variable points to a new String, the original object is untouched. To communicate a modified value back, the method must return the new String.
Interview Questions
Q1. Why is String immutable in Java?
Java made String immutable for five reasons. First, it enables the String Pool ā if strings were mutable, sharing them in a pool would be unsafe because one variable could change content seen by all others. Second, it provides thread safety ā immutable objects can be shared across threads without any synchronisation. Third, it makes Strings reliable as HashMap and HashSet keys ā the hashCode() never changes after creation. Fourth, it provides a security guarantee ā validated strings cannot be modified between validation and use. Fifth, it allows hashCode() to be cached after first computation ā safe only because the content never changes.
Q2. What does it mean for String to be immutable?
Once a String object is created, its character content is fixed and cannot be changed. The internal char[] (or byte[] in Java 9+) is private final ā it cannot be replaced or modified. No method exists to change a character at a position. Every method that appears to modify a String ā toUpperCase(), replace(), trim(), substring() ā creates and returns a brand new String object with the result. The original String is completely untouched.
Q3. What is the difference between String immutability and the final keyword on a variable?
String immutability means the String object's content cannot change. final on a variable means the variable cannot be reassigned to point to a different object. They are independent. final String name = "Priya" means name cannot point to a different String, AND the String's content cannot change. String name = "Priya" (without final) means the variable can be reassigned ā name = "Rohan" ā but the original "Priya" String object in the pool remains unchanged. Two orthogonal concepts.
Q4. Why is String declared as a final class?
To prevent subclassing. Without final, a developer could write class MutableString extends String and override methods to allow content modification. Any code that accepted a String would then have to worry about whether it actually received a mutable subtype. Making the class final closes this completely ā every String reference is guaranteed to be an instance of java.lang.String exactly, with all its immutability guarantees intact.
Q5. Why should you use char[] instead of String for passwords?
String is immutable ā you cannot clear its content after use. A password stored in a String remains in heap memory until garbage collected (unpredictable timing) and may persist in the String Pool indefinitely. During this window, a memory dump or reflection could expose it. A char[] can be explicitly zeroed ā Arrays.fill(chars, '\0') ā immediately after use, reducing the exposure window to near-zero. This is why security-conscious APIs like JPasswordField.getPassword() return char[].
Q6. Does immutability mean you cannot change a String variable in Java?
No. The variable can be reassigned to point to a different String ā name = name.toUpperCase() is valid. What cannot change is the content of the String object itself. After name = name.toUpperCase(), the variable name points to a new String with uppercase characters. The original String object that name previously pointed to is unchanged in memory ā it still exists until garbage collected. Immutability is a property of the object, not the variable.
FAQs
Can reflection be used to modify a String's internal char array?
Technically yes ā with setAccessible(true) on the private value field and direct array modification. But this is explicitly undefined behaviour, strongly discouraged, breaks the JVM's internal assumptions, and is illegal under the Java module system's strong encapsulation in Java 9+. In production code, String immutability is an absolute guarantee for all practical purposes.
If String is immutable, why does String concatenation work?
Concatenation does not modify either String ā it creates a new String that contains the characters of both. "Hello" + " World" produces a new String "Hello World". The original "Hello" and " World" strings are unchanged. The + operator is syntactic sugar that the compiler converts to StringBuilder.append().toString() behind the scenes.
Does immutability affect String comparison?
Yes, positively. Because a String's content never changes, hashCode() can be computed once and cached. This makes HashMap and HashSet lookups fast ā the hash does not need to be recomputed on every access. It also means two String variables with the same content always return the same hashCode() ā which satisfies the equals()āhashCode() contract reliably.
Is String immutability a Java-specific concept?
No. Many modern languages make their primary string type immutable ā including Python, Kotlin, Go, and Rust. The benefits of predictability, safety, and pool-sharing are language-agnostic. Java was among the earliest mainstream languages to enforce this with a final class and a private final internal array, making the guarantee not just a convention but a structural constraint.
What happens to the old String when you reassign a String variable?
The old String object becomes eligible for garbage collection if nothing else holds a reference to it. If the old String was a literal and is in the String Pool, it typically stays in the pool for the lifetime of the class that uses it ā pool entries are generally not collected during normal application execution. Heap String objects (created with new String() or runtime operations) are collected normally when unreferenced.
Summary
String immutability is not a limitation ā it is a carefully engineered guarantee that enables String Pool sharing, thread-safe concurrent access, stable HashMap keys, security isolation between validation and use, and efficient hashCode() caching. The final class declaration adds a structural lock that prevents subclasses from bypassing these guarantees.
The practical implications every developer should know: every String method that appears to modify text returns a new String ā always capture the return value. Variables can be reassigned; the objects they pointed to cannot be changed. Use char[] for passwords. Use StringBuilder for iterative building. Use static final String for application-wide constants.
For interviews, be ready to name all five reasons for immutability, explain the difference between the variable being reassigned and the object being mutated, explain why final on the class is necessary, and describe the char[] pattern for secure password handling.
What to Read Next
Practice Programs
Put this topic into practice ā each program has full code, output and a step-by-step explanation you can run online.
Reverse a string using a loop.
Converts every letter in a string to uppercase.
Replaces every occurrence of one character with another.