String Immutability in Java
Understand why Strings cannot be changed after creation
💡 Think of a String like a word written with a permanent marker on paper. Once you write it, you can't erase or change it! If you want a different word, you have to get a new piece of paper and write the new word. This is what 'immutable' means - it cannot be changed after it's created!
🔒 What is Immutability?
Immutability means that once a String object is created, its content cannot be modified. Any operation that seems to change a String actually creates a new String object!
public class ImmutabilityDemo { public static void main(String[] args) { String original = "Hello"; System.out.println("Original: " + original); // This looks like we're changing the string... original.toUpperCase(); // But the original is UNCHANGED! System.out.println("After toUpperCase(): " + original); // Still "Hello" // To use the modified version, we must assign it String modified = original.toUpperCase(); System.out.println("Modified: " + modified); // "HELLO" System.out.println("Original still: " + original); // Still "Hello" // Concatenation also creates NEW string String greeting = "Hello"; greeting.concat(" World"); // Creates new string but we ignore it System.out.println("After concat: " + greeting); // Still "Hello" // Must assign to use the new string greeting = greeting.concat(" World"); System.out.println("After assignment: " + greeting); // "Hello World" }}🤔 Why Are Strings Immutable?
Java designers made Strings immutable for several important reasons:
Security
Strings are used for sensitive data like usernames, passwords, and file paths. If Strings were mutable, malicious code could change them after validation!
Thread Safety
Immutable objects are automatically thread-safe. Multiple threads can use the same String without worrying about changes
String Pool Optimization
Java reuses identical String literals to save memory. This only works because Strings can't change!
Hashcode Caching
Strings' hashcode can be cached and reused because it never changes, making HashMap operations faster
public class WhyImmutable { public static void main(String[] args) { // SECURITY EXAMPLE String password = "secret123"; checkPassword(password); // password is still "secret123" - it couldn't be changed! // THREAD SAFETY EXAMPLE String shared = "Thread Safe"; // Multiple threads can read this safely without synchronization Thread t1 = new Thread(() -> System.out.println(shared)); Thread t2 = new Thread(() -> System.out.println(shared)); t1.start(); t2.start(); // STRING POOL EXAMPLE String s1 = "Hello"; // Created in pool String s2 = "Hello"; // Reuses same object from pool System.out.println(s1 == s2); // true - same object! // HASHCODE CACHING String key = "myKey"; int hash1 = key.hashCode(); // Since String is immutable, hashCode is calculated once and cached int hash2 = key.hashCode(); // Returns cached value - very fast! System.out.println("Same hashcode: " + (hash1 == hash2)); } static void checkPassword(String pwd) { // Even if we wanted to change pwd here, we can't! // This protects the original password string if (pwd.equals("secret123")) { System.out.println("Password correct"); } }}✨ Benefits of Immutability
- ✓Safe to share across multiple parts of your program
- ✓Can be used as keys in HashMap and HashSet
- ✓Thread-safe by default - no synchronization needed
- ✓Predictable behavior - strings won't change unexpectedly
- ✓Memory efficient through String pooling
import java.util.HashMap;public class BenefitsDemo { public static void main(String[] args) { // Benefit 1: Safe to share String message = "Important Data"; processData(message); System.out.println("Message unchanged: " + message); // Benefit 2: Can be used as HashMap key HashMap<String, Integer> scores = new HashMap<>(); String name = "Alice"; scores.put(name, 100); // Even if we do operations on name, the key in HashMap is safe! name = name.toUpperCase(); System.out.println("Score for 'Alice': " + scores.get("Alice")); // Still works! // Benefit 3: String pooling saves memory String a = "Java"; String b = "Java"; String c = "Java"; // All three reference the SAME object in memory! System.out.println("Same object: " + (a == b && b == c)); } static void processData(String data) { // Can't accidentally modify the caller's string data = data + " processed"; System.out.println("Inside method: " + data); // Original string in main() is unchanged! }}🔧 How to 'Modify' Strings
Since Strings are immutable, methods that seem to modify them actually return NEW strings:
public class ModifyingStrings { public static void main(String[] args) { // WRONG WAY - Ignoring return value String text = "hello world"; text.toUpperCase(); // Creates new string but we ignore it! System.out.println(text); // Still "hello world" ❌ // RIGHT WAY - Assign the result text = text.toUpperCase(); // Assign the new string System.out.println(text); // Now "HELLO WORLD" ✓ // Multiple modifications - each creates new object String name = " john doe "; name = name.trim(); // Remove whitespace name = name.toUpperCase(); // Convert to uppercase name = name.replace(" ", "_"); // Replace spaces System.out.println(name); // "JOHN_DOE" // Method chaining - more efficient String name2 = " jane smith "; name2 = name2.trim().toUpperCase().replace(" ", "_"); System.out.println(name2); // "JANE_SMITH" // Concatenation creates new objects String result = "Hello"; result = result + " "; // New object 1 result = result + "World"; // New object 2 System.out.println(result); // "Hello World" }}💾 Memory Impact of Immutability
public class MemoryImpact { public static void main(String[] args) { // BAD: Creates many temporary String objects String bad = ""; for (int i = 0; i < 5; i++) { bad = bad + i + " "; // Creates new String each iteration! // Iteration 0: "0 " // Iteration 1: "0 " + "1 " = "0 1 " (new object) // Iteration 2: "0 1 " + "2 " = "0 1 2 " (new object) // etc... Many temporary objects created! } System.out.println("Result: " + bad); // GOOD: Use StringBuilder (we'll learn more about this later) StringBuilder good = new StringBuilder(); for (int i = 0; i < 5; i++) { good.append(i).append(" "); // Modifies same object } System.out.println("Result: " + good.toString()); // Visualizing object creation String demo = "A"; System.out.println("Original object: " + System.identityHashCode(demo)); demo = demo + "B"; // New object created System.out.println("After +B: " + System.identityHashCode(demo)); demo = demo + "C"; // Another new object System.out.println("After +C: " + System.identityHashCode(demo)); // Notice: different hash codes = different objects! }}🔄 String vs Mutable Objects
import java.util.ArrayList;public class StringVsMutable { public static void main(String[] args) { // IMMUTABLE - String String immutable = "Hello"; modifyString(immutable); System.out.println("After method: " + immutable); // Still "Hello" // MUTABLE - ArrayList ArrayList<String> mutable = new ArrayList<>(); mutable.add("Hello"); modifyList(mutable); System.out.println("After method: " + mutable); // Changed to [Hello, World]! // IMMUTABLE - new assignment doesn't affect reference String s1 = "Original"; String s2 = s1; s1 = "Modified"; System.out.println("s1: " + s1); // "Modified" System.out.println("s2: " + s2); // Still "Original" // MUTABLE - changes affect all references ArrayList<String> list1 = new ArrayList<>(); list1.add("Original"); ArrayList<String> list2 = list1; // Same object reference list1.add("Modified"); System.out.println("list1: " + list1); // [Original, Modified] System.out.println("list2: " + list2); // [Original, Modified] - same! } static void modifyString(String s) { s = s + " World"; // Creates new object, doesn't affect caller } static void modifyList(ArrayList<String> list) { list.add("World"); // Modifies the actual object! }}🔑 Key Concepts
New Object Created
Every string modification creates a new String object
String s = "Hi"; s = s + "!"; // new object created
Original Unchanged
The original String remains unchanged in memory
String s = "Hello"; s.toUpperCase(); // s is still "Hello"
Must Reassign
You must assign the result to use the modified string
String s = "hi"; s = s.toUpperCase(); // now s is "HI"
Memory Consideration
Many modifications create many objects; use StringBuilder for loops
Use StringBuilder when concatenating in loops
✨ Best Practices
- ✓Use StringBuilder or StringBuffer for multiple modifications
- ✓Don't concatenate strings in loops - creates too many objects
- ✓Take advantage of String pooling by using literals when possible
- ✓Remember to assign the result of string methods to a variable
- ✓Use immutability to your advantage for thread-safe code
⚠️ Common Mistakes
- ✗Calling a method and ignoring the return value (str.trim() without assignment)
- ✗Concatenating strings in loops instead of using StringBuilder
- ✗Thinking that string methods modify the original string
- ✗Creating unnecessary String objects when StringBuilder would be better
- ✗Not understanding the memory implications of string concatenation
💼 Interview Tips
- •Be able to explain why Strings are immutable (security, thread safety, pooling)
- •Know that every string operation creates a new object
- •Understand the performance impact of string concatenation in loops
- •Know when to use String vs StringBuilder vs StringBuffer
- •Be able to explain String pooling and its relationship to immutability
- •Understand that final keyword on String class prevents inheritance, not immutability