Java Tutorial
🔍

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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
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.

Java
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:

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.

Java
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.

Java
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

AspectBenefit of ImmutabilityTradeoff
String PoolSafe sharing — multiple variables point to one objectCannot modify shared entry
Thread safetyNo synchronisation needed for concurrent readsCannot update a shared String value in place
HashMap keysStable hashCode — reliable lookups alwaysCannot use as a key when content must evolve
SecurityValidated value cannot be changed after validationWorkarounds needed for secure mutable text (char[])
HashCode cachingComputed once, reused forever — fast repeated hashingSlight memory overhead to store cached value
Subclassingfinal class prevents subtype attackCannot extend String for custom behaviour
Memory in loopsPredictable — every concat creates a known new objectMore GC pressure if building strings iteratively
Method return safetyCannot leak internal state — every method returns a copyCallers 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.

Java
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}
Java
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}
Java
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

Java
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

Java
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 gone

Mistake 3 — Using += in a Loop

Java
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 end

Mistake 4 — Confusing Variable Reassignment With Object Mutation

Java
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 it

A 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

TopicLink
How the String Pool uses immutability to safely share String objectsJava String Pool →
How StringBuilder provides mutable string building when immutability is a constraintJava StringBuilder →
How equals() and == behave differently because String overrides equals()Java String Comparison →
How immutability connects to designing your own immutable classesJava Immutable Class →
How HashMap and HashSet depend on stable hashCode — which String providesJava HashMap →
Java Immutable Strings | DevStackFlow