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.
1// File: ImmutabilityProof.java
2
3public class ImmutabilityProof {
4
5 public static void main(String[] args) {
6
7 String original = " hello, world! ";
8
9 System.out.println("=== Every method returns a NEW String ===");
10 System.out.println("Original : '" + original + "'");
11
12 // None of these modify 'original' — each returns a new object
13 String trimmed = original.trim();
14 String upper = original.toUpperCase();
15 String replaced = original.replace("world", "Java");
16 String substring = original.substring(2, 7);
17
18 System.out.println("After trim() : '" + trimmed + "'");
19 System.out.println("After upper() : '" + upper + "'");
20 System.out.println("After replace() : '" + replaced + "'");
21 System.out.println("After substring : '" + substring + "'");
22 System.out.println();
23 System.out.println("Original STILL : '" + original + "'"); // unchanged
24
25 System.out.println();
26
27 // Identity hash codes prove they are different objects
28 System.out.println("=== Different Objects ===");
29 System.out.println("original id : " + System.identityHashCode(original));
30 System.out.println("trimmed id : " + System.identityHashCode(trimmed)); // different
31 System.out.println("upper id : " + System.identityHashCode(upper)); // different
32 System.out.println("replaced id : " + System.identityHashCode(replaced)); // different
33
34 System.out.println();
35
36 // The most common beginner mistake — calling method without capturing result
37 System.out.println("=== The Most Common Mistake ===");
38 String username = " rohan_mehta ";
39 username.trim(); // result thrown away — username unchanged
40 username.toUpperCase(); // result thrown away — username unchanged
41 System.out.println("Without capture: '" + username + "'"); // still padded
42
43 // Correct — always capture the return value
44 username = username.trim().toUpperCase();
45 System.out.println("With capture : '" + username + "'"); // correct
46 }
47}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.
1// File: PoolSafetyDemo.java
2
3public class PoolSafetyDemo {
4
5 public static void main(String[] args) {
6
7 // Both point to the SAME pool entry
8 String city1 = "Delhi";
9 String city2 = "Delhi";
10
11 System.out.println("city1 == city2 : " + (city1 == city2)); // true — same object
12
13 // If Strings were mutable (hypothetical — this does NOT work in Java):
14 // city1.setCharAt(0, 'B'); // would change "Delhi" to "Belhi"
15 // System.out.println(city2); // would print "Belhi" — silent corruption!
16
17 // Because String is immutable:
18 city1 = city1.replace("Delhi", "Mumbai"); // creates NEW String — pool entry unchanged
19 System.out.println("city1 : " + city1); // Mumbai
20 System.out.println("city2 : " + city2); // Delhi — still safe in the pool
21 }
22}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.
1// File: ThreadSafetyDemo.java
2
3public class ThreadSafetyDemo {
4
5 // This String is shared by multiple threads
6 private static final String CONFIG_URL = "https://api.meesho.com/v2";
7
8 public static void main(String[] args) throws InterruptedException {
9
10 Runnable task = () -> {
11 // All threads read the same String — completely safe
12 // No thread can modify CONFIG_URL — it is immutable
13 String processed = CONFIG_URL.toUpperCase(); // creates a NEW String per thread
14 System.out.println(Thread.currentThread().getName()
15 + " → URL: " + CONFIG_URL
16 + " | Upper: " + processed);
17 };
18
19 Thread[] threads = new Thread[4];
20 for (int i = 0; i < threads.length; i++) {
21 threads[i] = new Thread(task, "Worker-" + (i + 1));
22 }
23 for (Thread t : threads) t.start();
24 for (Thread t : threads) t.join();
25
26 // CONFIG_URL is exactly what it was — no thread could have changed it
27 System.out.println("\nCONFIG_URL intact: " + CONFIG_URL);
28 }
29}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.
1// File: HashMapKeyDemo.java
2
3import java.util.HashMap;
4
5public class HashMapKeyDemo {
6
7 public static void main(String[] args) {
8
9 // String as HashMap key — always safe because String is immutable
10 HashMap<String, String> userRoles = new HashMap<>();
11 String userId = "USR-001";
12
13 userRoles.put(userId, "ADMIN");
14
15 // Even if we "change" userId, the original String is untouched
16 // userId = "USR-002" only changes the VARIABLE, not the String in the map
17 userId = "USR-002"; // variable now points to a different String
18
19 // The map still has USR-001 because String is immutable
20 System.out.println("Get USR-001: " + userRoles.get("USR-001")); // ADMIN
21 System.out.println("Get USR-002: " + userRoles.get("USR-002")); // null
22
23 // Why this matters — if String were mutable like StringBuilder:
24 // The hashCode of the key would change, making the entry unfindable
25 // Immutability guarantees the hash never changes after insertion
26
27 System.out.println();
28 System.out.println("Map is safe because String hashCode never changes.");
29 System.out.println("String 'USR-001' hashCode: " + "USR-001".hashCode());
30 System.out.println("Same value, same hash always: " +
31 ("USR-001".hashCode() == "USR-001".hashCode())); // always true
32 }
33}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.
1// File: SecurityDemo.java
2
3public class SecurityDemo {
4
5 // Validates and opens a file
6 public static void openSecureFile(String filePath) {
7 // Step 1 — validate
8 if (!filePath.startsWith("/secure/") || filePath.contains("..")) {
9 throw new SecurityException("Access denied: " + filePath);
10 }
11
12 // If String were mutable, another thread could change filePath
13 // between the validation above and the use below
14 // e.g., change "/secure/report.pdf" to "/etc/passwd"
15
16 // Step 2 — use — safe because String is immutable
17 System.out.println("Opening file: " + filePath);
18 // actual file operation would go here
19 }
20
21 public static void main(String[] args) {
22
23 // Safe — filePath cannot be changed between validation and use
24 String path = "/secure/reports/q4-sales.pdf";
25 openSecureFile(path);
26
27 // This creates a NEW String — the old validated path is untouched
28 path = "/secure/reports/q3-sales.pdf";
29 openSecureFile(path);
30
31 System.out.println();
32 System.out.println("Security benefit: validated value cannot be changed");
33 System.out.println("after validation by any other code or thread.");
34 }
35}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.
1// File: HashCodeCacheDemo.java
2
3public class HashCodeCacheDemo {
4
5 public static void main(String[] args) {
6
7 String text = "DevStackFlow";
8
9 // First call — computes and caches the hash
10 long start1 = System.nanoTime();
11 int hash1 = text.hashCode();
12 long time1 = System.nanoTime() - start1;
13
14 // Subsequent calls — returns cached value immediately
15 long start2 = System.nanoTime();
16 int hash2 = text.hashCode();
17 long time2 = System.nanoTime() - start2;
18
19 System.out.println("hash1 : " + hash1);
20 System.out.println("hash2 : " + hash2);
21 System.out.println("Same : " + (hash1 == hash2)); // always true
22 System.out.println();
23 System.out.println("This caching is safe only because the String cannot change.");
24 System.out.println("If characters could be modified, the cached hash would be wrong.");
25 }
26}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:
1public 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.
1// File: FinalClassDemo.java
2
3public class FinalClassDemo {
4
5 public static void main(String[] args) {
6
7 // You cannot extend String
8 // class CustomString extends String { } // compile error
9
10 // You cannot create a mutable subtype that disguises as String
11 // This is intentional — it protects all code that trusts String immutability
12
13 // What final on String guarantees:
14 // 1. String's behaviour cannot be overridden by subclasses
15 // 2. Every String IS-A String — no tricks, no surprises
16 // 3. Libraries can trust that any String parameter behaves as documented
17
18 System.out.println("String is final — cannot be subclassed.");
19 System.out.println("This protects immutability at the language level.");
20
21 // Demonstrate final field inside String (conceptually)
22 // The internal char[] value is private final — cannot be replaced
23 String s = "hello";
24 System.out.println("String s: " + s);
25 // s.value = new char[]{'w','o','r','l','d'}; // not possible — private final
26 System.out.println("Content is sealed at: " + System.identityHashCode(s));
27 }
28}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.
1// File: VariableVsObjectDemo.java
2
3public class VariableVsObjectDemo {
4
5 public static void main(String[] args) {
6
7 String message = "Hello";
8 System.out.println("message id: " + System.identityHashCode(message));
9
10 // Reassignment — the VARIABLE changes, the original OBJECT does not
11 message = "Hello" + ", World"; // creates a NEW String
12 System.out.println("message id: " + System.identityHashCode(message)); // different id
13
14 // The String "Hello" still exists in the pool — it was not modified
15 String hello = "Hello"; // retrieves the SAME pooled "Hello" from before
16 System.out.println("'Hello' id: " + System.identityHashCode(hello));
17
18 System.out.println();
19
20 // What changes: What does NOT change:
21 // ───────────────────── ────────────────────────────────────────
22 // Which object the The content of any String object
23 // variable points to The characters in the char[] array
24 // The hashCode of any String object
25 // The String Pool entries
26
27 // Practical impact:
28 String url = "https://api.razorpay.com";
29 processUrl(url);
30 System.out.println("After method call: " + url); // unchanged — method cannot modify it
31 }
32
33 static void processUrl(String u) {
34 // u = u.replace("razorpay", "malicious"); // only changes local variable u
35 // caller's 'url' is completely unaffected
36 System.out.println("Processing: " + u.toUpperCase()); // returns new String
37 }
38}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 |
Real-World Example — Payment Gateway URL and Credential Handler
The Business Problem
A payment gateway integration at a company like Razorpay or PhonePe stores base URLs, API keys, and merchant IDs as Strings. These are read across multiple threads — authentication middleware, request signers, response validators — all simultaneously. Because Strings are immutable, all reads are safe with no locks. The values are also used as HashMap keys for routing configuration. The immutability guarantee ensures that a value validated in one layer cannot be modified by another layer before it reaches its destination.
1// File: GatewayConfig.java
2
3public final class GatewayConfig {
4
5 // Immutable String constants — shared freely across all threads
6 public static final String BASE_URL = "https://api.razorpay.com/v1";
7 public static final String SANDBOX_URL = "https://api.sandbox.razorpay.com/v1";
8 public static final String PAYMENT_PATH = "/payments";
9 public static final String REFUND_PATH = "/refunds";
10 public static final String WEBHOOK_PATH = "/webhooks";
11
12 private final String merchantId;
13 private final String environment;
14
15 public GatewayConfig(String merchantId, String environment) {
16 // Defensive copy — even though String is immutable, making intent explicit
17 this.merchantId = merchantId;
18 this.environment = environment;
19 }
20
21 public String getMerchantId() { return merchantId; }
22 public String getEnvironment() { return environment; }
23
24 public String getBaseUrl() {
25 // No risk — SANDBOX_URL and BASE_URL are immutable constants
26 return "SANDBOX".equals(environment) ? SANDBOX_URL : BASE_URL;
27 }
28
29 public String buildEndpoint(String path) {
30 // String concatenation — creates a new String, original constants unchanged
31 return getBaseUrl() + path;
32 }
33}1// File: PaymentRequestBuilder.java
2
3public class PaymentRequestBuilder {
4
5 private final GatewayConfig config;
6
7 public PaymentRequestBuilder(GatewayConfig config) {
8 this.config = config;
9 }
10
11 // Returns a validated, immutable String — caller cannot tamper with it after
12 public String buildPaymentUrl(String orderId, double amount) {
13 // Validate first
14 if (orderId == null || orderId.isBlank()) {
15 throw new IllegalArgumentException("Order ID is required.");
16 }
17 if (amount <= 0) {
18 throw new IllegalArgumentException("Amount must be positive.");
19 }
20
21 // Build — each piece is a String constant or the result of a safe method
22 // The returned String cannot be changed by the caller after this returns
23 return config.buildEndpoint(GatewayConfig.PAYMENT_PATH)
24 + "?merchant=" + config.getMerchantId()
25 + "&order=" + orderId
26 + "&amount=" + (int)(amount * 100); // paise
27 }
28
29 public String buildRefundUrl(String paymentId) {
30 if (paymentId == null || paymentId.isBlank()) {
31 throw new IllegalArgumentException("Payment ID is required.");
32 }
33 return config.buildEndpoint(GatewayConfig.REFUND_PATH)
34 + "/" + paymentId;
35 }
36}1// File: PaymentGatewayDemo.java
2
3import java.util.HashMap;
4import java.util.Map;
5import java.util.concurrent.ExecutorService;
6import java.util.concurrent.Executors;
7import java.util.concurrent.TimeUnit;
8
9public class PaymentGatewayDemo {
10
11 public static void main(String[] args) throws InterruptedException {
12
13 System.out.println("╔══════════════════════════════════════════╗");
14 System.out.println("║ PAYMENT GATEWAY — IMMUTABILITY DEMO ║");
15 System.out.println("╚══════════════════════════════════════════╝\n");
16
17 GatewayConfig liveConfig = new GatewayConfig("MID-12345", "LIVE");
18 GatewayConfig sandboxConfig = new GatewayConfig("MID-TEST", "SANDBOX");
19 PaymentRequestBuilder liveBuilder = new PaymentRequestBuilder(liveConfig);
20 PaymentRequestBuilder sandboxBuilder = new PaymentRequestBuilder(sandboxConfig);
21
22 // 1 — Build URLs — each call returns a new immutable String
23 System.out.println("=== URL Building ===");
24 String payUrl = liveBuilder.buildPaymentUrl("ORD-001", 1299.50);
25 String refUrl = liveBuilder.buildRefundUrl("PAY-789");
26 String sandPay = sandboxBuilder.buildPaymentUrl("ORD-TEST-001", 499.00);
27
28 System.out.println("Live payment : " + payUrl);
29 System.out.println("Live refund : " + refUrl);
30 System.out.println("Sandbox pay : " + sandPay);
31
32 System.out.println();
33
34 // 2 — Use Strings as HashMap keys — safe because String is immutable
35 System.out.println("=== HashMap with String Keys ===");
36 Map<String, String> routeTable = new HashMap<>();
37 routeTable.put(GatewayConfig.PAYMENT_PATH, "PaymentController");
38 routeTable.put(GatewayConfig.REFUND_PATH, "RefundController");
39 routeTable.put(GatewayConfig.WEBHOOK_PATH, "WebhookController");
40
41 System.out.println("Route /payments → " + routeTable.get("/payments"));
42 System.out.println("Route /refunds → " + routeTable.get("/refunds"));
43
44 System.out.println();
45
46 // 3 — Multi-threaded access — immutability means no locks needed
47 System.out.println("=== Multi-threaded URL Access ===");
48 ExecutorService pool = Executors.newFixedThreadPool(4);
49
50 for (int i = 1; i <= 4; i++) {
51 final int ordNum = i;
52 pool.submit(() -> {
53 // All threads can safely read GatewayConfig.BASE_URL — immutable
54 String url = liveBuilder.buildPaymentUrl(
55 "ORD-" + String.format("%03d", ordNum),
56 ordNum * 500.0);
57 System.out.println(Thread.currentThread().getName() + " → " + url);
58 });
59 }
60
61 pool.shutdown();
62 pool.awaitTermination(5, TimeUnit.SECONDS);
63
64 System.out.println();
65 System.out.println("=== Constants unchanged after all operations ===");
66 System.out.println("BASE_URL : " + GatewayConfig.BASE_URL); // unchanged
67 System.out.println("SANDBOX_URL : " + GatewayConfig.SANDBOX_URL); // unchanged
68 }
69}Output:
╔══════════════════════════════════════════╗
║ PAYMENT GATEWAY — IMMUTABILITY DEMO ║
╚══════════════════════════════════════════╝
=== URL Building ===
Live payment : https://api.razorpay.com/v1/payments?merchant=MID-12345&order=ORD-001&amount=129950
Live refund : https://api.razorpay.com/v1/refunds/PAY-789
Sandbox pay : https://api.sandbox.razorpay.com/v1/payments?merchant=MID-TEST&order=ORD-TEST-001&amount=49900
=== HashMap with String Keys ===
Route /payments → PaymentController
Route /refunds → RefundController
=== Multi-threaded URL Access ===
pool-1-thread-1 → https://api.razorpay.com/v1/payments?merchant=MID-12345&order=ORD-001&amount=50000
pool-1-thread-2 → https://api.razorpay.com/v1/payments?merchant=MID-12345&order=ORD-002&amount=100000
pool-1-thread-3 → https://api.razorpay.com/v1/payments?merchant=MID-12345&order=ORD-003&amount=150000
pool-1-thread-4 → https://api.razorpay.com/v1/payments?merchant=MID-12345&order=ORD-004&amount=200000
=== Constants unchanged after all operations ===
BASE_URL : https://api.razorpay.com/v1
SANDBOX_URL : https://api.sandbox.razorpay.com/v1
Four threads all read the same BASE_URL constant without any synchronized block. Zero risk of corruption. The URL and routing constants are used as HashMap keys reliably. Every URL built by buildPaymentUrl() is a new immutable String that the caller cannot change after receiving it.
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
1String phone = " 98765-43210 ";
2
3phone.trim(); // returns trimmed String — but it is thrown away
4phone.replace("-", ""); // returns cleaned String — also thrown away
5
6System.out.println(phone); // " 98765-43210 " — still unchanged
7
8// Fix — capture every return value
9phone = phone.trim().replace("-", "");
10System.out.println(phone); // "9876543210"Mistake 2 — Storing Passwords as String
1// BAD — String stays in memory, in pool, cannot be cleared
2String password = getPasswordFromUser(); // might be pooled
3verifyPassword(password);
4password = null; // does not wipe the String from memory
5
6// GOOD — char[] can be explicitly cleared
7char[] passwordChars = getPasswordCharsFromUser();
8verifyPassword(new String(passwordChars)); // convert only when needed
9java.util.Arrays.fill(passwordChars, '\0'); // zero out — sensitive data goneMistake 3 — Using += in a Loop
1// Immutability makes this O(n²) — creates a new String every iteration
2String output = "";
3for (int i = 0; i < 10000; i++) {
4 output += "item" + i + "\n"; // 10,000 String objects created and discarded
5}
6
7// Fix — use StringBuilder, call toString() once
8StringBuilder sb = new StringBuilder();
9for (int i = 0; i < 10000; i++) {
10 sb.append("item").append(i).append("\n");
11}
12String output = sb.toString(); // one String created at the endMistake 4 — Confusing Variable Reassignment With Object Mutation
1void process(String value) {
2 value = value.trim(); // only changes local parameter — caller unchanged
3 System.out.println("Inside: " + value); // trimmed
4}
5
6String data = " hello ";
7process(data);
8System.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
| Topic | Link |
|---|---|
| How the String Pool uses immutability to safely share String objects | Java String Pool → |
| How StringBuilder provides mutable string building when immutability is a constraint | Java StringBuilder → |
| How equals() and == behave differently because String overrides equals() | Java String Comparison → |
| How immutability connects to designing your own immutable classes | Java Immutable Class → |
| How HashMap and HashSet depend on stable hashCode — which String provides | Java HashMap → |