☕ Java
Immutable String
String immutability is the most important design decision in Java's String class. Once a String object is created, its character sequence can never change. No method on String modifies the string; every method that appears to modify returns a new String object containing the result. This design decision drives thread safety, enables the string pool, makes strings safe hash map keys, and simplifies reasoning about string values. Understanding why String is immutable, how immutability is enforced, and what the consequences of immutability are clarifies the behaviour of virtually every piece of Java code that handles strings.
What Immutability Means for String
A String object's character sequence is fixed at construction time and never changes. The private char[] (or byte[] in Java 9+) backing field is not accessible outside the class, the class is final so it cannot be subclassed, and no method of String modifies the backing array. Every method that sounds like it modifies — replace(), toUpperCase(), trim(), concat(), substring() — returns a new String object with the transformed content while leaving the original unchanged.
This distinction between "appears to change" and "actually changes" is critical for understanding string code. String s = "hello"; s = s.toUpperCase(); does not modify the object that s originally pointed to. It creates a new object containing "HELLO" and reassigns the variable s to point to this new object. The original "hello" object is unchanged, and if nothing else points to it, it becomes eligible for garbage collection. The variable changed; the object did not.
Immutability is enforced at the class level through three mechanisms: the private final char[]/byte[] backing field cannot be accessed from outside the class; the String class is declared final so no subclass can gain access to the backing field and add mutating methods; and the String class's own methods never modify the backing array. Together these three guarantees make it impossible to change a String object's content from any code outside the String class itself.
Java
// ── String methods return NEW objects — original unchanged ───────────
String original = "Hello World";
String upper = original.toUpperCase(); // new object
String lower = original.toLowerCase(); // new object
String trimmed = " Hello ".trim(); // new object
String replaced = original.replace("World", "Java"); // new object
String sub = original.substring(6); // new object
System.out.println(original); // "Hello World" — completely unchanged
System.out.println(upper); // "HELLO WORLD"
System.out.println(lower); // "hello world"
System.out.println(replaced); // "Hello Java"
System.out.println(sub); // "World"
// ── Variable reassignment vs object mutation ──────────────────────────
String s = "hello";
String original_ref = s; // save reference to original object
s = s.toUpperCase(); // s now points to new object "HELLO"
System.out.println(s); // "HELLO" — s points to new object
System.out.println(original_ref); // "hello" — original object unchanged
// ── Consequence: long chains create many temporary objects ─────────────
String result = " Hello World "
.trim() // new object: "Hello World"
.toLowerCase() // new object: "hello world"
.replace(" ", "_") // new object: "hello_world"
.concat("!"); // new object: "hello_world!"
// Four intermediate String objects created, three immediately unreachable
// ── Immutability enforced by three mechanisms ─────────────────────────
// 1. private final byte[]/char[] value — no external access
// 2. final class — no subclass can expose or modify value
// 3. no mutating methods — String's own API never modifies value
//
// class String { (simplified)
// private final byte[] value; ← private, final
// private final byte coder;
// private int hash; ← only mutable field, and it's safe
// ...
// }Why String is Immutable — The Design Reasons
The decision to make String immutable was deliberate and driven by several important requirements. The first and most compelling reason is the string pool. The pool works only because all pooled strings are guaranteed never to change. If String were mutable, two variables sharing a pool entry could interfere with each other — one modifying the shared string through its variable, unexpectedly changing the value seen through the other variable. The pool requires that sharing a string is always safe, which requires immutability.
The second reason is thread safety. Immutable objects are inherently thread-safe because they have no mutable state that threads could race to modify. String is one of the most shared objects in any Java program — strings are passed as arguments, stored in collections, used as keys — and thread safety by construction (not by synchronisation) is a major advantage. Multiple threads can safely share, read, and pass the same String object without any synchronisation overhead.
The third reason is security. String is used to represent passwords, file paths, network addresses, database URLs, and class names used in reflection. If String were mutable, a caller could pass a path to a security check, have the check approve it, and then mutate the path before the actual operation — a classic time-of-check to time-of-use (TOCTOU) vulnerability. Immutability ensures the string passed to a security check is exactly the string used in the operation.
The fourth reason is hash code reliability. HashMap, HashSet, and other hash-based structures use the hash code as a lookup key. If a string used as a HashMap key were mutable, changing it after insertion would corrupt the map — the entry would be stored under the old hash code but the key would now produce a different hash, making the entry unfindable. Immutability guarantees that a string's hash code never changes, making it a reliable and permanent hash map key.
Java
// ── Reason 1: String pool requires immutability ──────────────────────
// If String were mutable (hypothetical broken design):
// String a = "hello"; // pool entry
// String b = "hello"; // same pool entry (shared object!)
// a.setChar(0, 'J'); // mutating a would also change b!
// System.out.println(b); // "jello" — unexpected!
// Immutability prevents this sharing hazard
// ── Reason 2: Thread safety without synchronization ───────────────────
String config = loadFromFile("config.properties");
// Multiple threads can safely read config simultaneously
Thread t1 = new Thread(() -> process(config)); // no sync needed
Thread t2 = new Thread(() -> validate(config)); // no sync needed
Thread t3 = new Thread(() -> log(config)); // no sync needed
t1.start(); t2.start(); t3.start();
// No race conditions — config can never change
// ── Reason 3: Security — TOCTOU prevention ────────────────────────────
// Mutable string vulnerability (hypothetical):
// class FileManager {
// public void open(MutableString path) {
// securityCheck(path); // path = "/safe/file.txt" — approved
// // Attacker mutates path on another thread:
// // path → "/etc/passwd"
// openFile(path); // now opens restricted file!
// }
// }
//
// With immutable String:
public void open(String path) {
securityCheck(path); // path = "/safe/file.txt" — approved
openFile(path); // guaranteed SAME value — no race possible
}
// ── Reason 4: Reliable hash map keys ─────────────────────────────────
Map<String, Integer> wordCount = new HashMap<>();
String key = "hello";
wordCount.put(key, 1);
// key's hash code cannot change → map lookup always works
System.out.println(wordCount.get("hello")); // 1
System.out.println(wordCount.get(key)); // 1 — always finds it
// If String were mutable (hypothetical):
// key.setChar(0, 'J'); // key is now "jello"
// wordCount.get("jello") would not find it (stored under "hello" hash)
// wordCount.get("hello") would not find it either (key changed)
// The entry would be permanently lost in the map!Immutability and Memory — Object Creation Patterns
String immutability has a direct consequence on memory: every operation that produces a modified string creates a new object. For simple operations — trimming whitespace, converting case, replacing one occurrence — the cost is one allocation and the work proportional to the string length. For repeated operations in loops — building a string by appending characters or accumulating records — naive string concatenation creates O(n) intermediate objects that are all immediately unreachable, causing significant GC pressure.
The pattern for efficient string construction is explicit use of StringBuilder, which is mutable. Build the string in a StringBuilder, call toString() once at the end to create the final immutable String. This pattern creates exactly two objects: the StringBuilder and the final String. The naive += approach in a loop creates O(n) String objects.
Java 11 introduced several new String methods (strip(), stripLeading(), stripTrailing(), isBlank(), lines(), repeat()) that are efficient and match modern Unicode whitespace handling. Understanding which operations create new objects and which reuse existing ones is important for writing memory-efficient string code.
Java
// ── Object creation cost of chained operations ───────────────────────
String input = " Hello, World! ";
// Each method creates a new String object:
String result = input
.strip() // object 1: "Hello, World!"
.toLowerCase() // object 2: "hello, world!"
.replace(",", "") // object 3: "hello world!"
.replace("!", "");// object 4: "hello world"
// Objects 1, 2, 3 immediately unreachable — GC fodder
// ── String building patterns ──────────────────────────────────────────
List<String> items = List.of("alpha", "beta", "gamma", "delta");
// WRONG — O(n²) object creation:
String joined = "";
for (String item : items) {
joined += item + ", "; // new String each iteration
}
// CORRECT — one StringBuilder, one final String:
StringBuilder sb = new StringBuilder();
for (String item : items) {
sb.append(item).append(", ");
}
String efficient = sb.toString();
// BEST for this case — purpose-built method:
String best = String.join(", ", items);
// ── Java 11+ String methods ───────────────────────────────────────────
// strip() uses Unicode whitespace definition (broader than trim())
" Hello
".strip(); // "Hello" — removes all Unicode whitespace
" Hello
".stripLeading(); // "Hello
"
" Hello
".stripTrailing(); // " Hello"
" ".isBlank(); // true — empty after stripping
// lines() — lazy stream of lines, any line separator
"line1
line2
line3".lines()
.forEach(System.out::println); // line1, line2, line3
// repeat() — repetition
"ab".repeat(3); // "ababab"
"-".repeat(40); // "----------------------------------------"
// ── Sharing substrings — no copying since Java 7 ──────────────────────
// Before Java 7: substring() shared the original char[] with an offset
// Since Java 7: substring() always creates a new String with its own array
// Implication: substring() in a tight loop creates many objects
// For performance-sensitive parsing, consider manual index tracking
// instead of extracting substrings for every tokenRelated Topics in Strings
String Class
String is one of the most fundamental classes in Java — used in virtually every program, yet deeply misunderstood by many developers. A String represents an immutable sequence of Unicode characters. It is not a primitive type but a full class in java.lang, automatically imported into every Java file. Understanding String means understanding how it is stored in memory, why it is immutable, how the string pool works, what the difference between == and equals() means for strings, and how to use the class efficiently. This entry covers String's nature as a class, its internal representation, the critical distinction between reference equality and value equality, String's place in the type hierarchy, and the design decisions that make String behave the way it does.
String Pool
The string pool (also called the string intern pool or string constant pool) is a special memory region maintained by the JVM that stores a single copy of each unique string value. When two string literals have the same content, they refer to the same object in the pool rather than two separate objects. The pool is a flyweight pattern applied at the language level — it dramatically reduces memory consumption in applications that use many repeated string values, which is nearly every application. This entry covers how the pool works, where it lives in JVM memory, how to interact with it programmatically, the intern() method, performance implications, and when to use or avoid pool entries.
Mutable String
Java provides two mutable string classes for scenarios where String's immutability would be inefficient: StringBuilder and StringBuffer. Both maintain an internal character buffer that can be modified in place — characters can be appended, inserted, deleted, and replaced without creating new objects. StringBuilder is the modern choice for single-threaded use; StringBuffer is the legacy thread-safe version with synchronised methods. This entry covers the internal buffer mechanics, the full API of both classes, performance characteristics, when to use each, thread safety implications, and the complete patterns for efficient string construction.
String Methods
The String class provides over 60 methods covering character inspection, searching, comparison, transformation, splitting, joining, formatting, and encoding. Knowing these methods well eliminates the need to write manual character-by-character loops for common string operations and prevents the common mistake of reimplementing logic that the library already provides efficiently. This entry covers every major method category with precise semantics, edge cases, performance characteristics, and practical examples — from the fundamental length() and charAt() through to the modern Java 11+ additions like strip(), isBlank(), lines(), and repeat().