Historical Context & Motivation
When James Gosling and the Oak team at Sun Microsystems designed Java in the early 1990s, they faced a fundamental question: should text data be changeable after it is created? Languages like C gave programmers direct access to character arrays, which made strings flexible but also introduced serious bugs — buffer overflows, dangling pointers, and unpredictable behavior when two parts of a program shared the same text. The Java designers chose a different path. They made the String class immutable, meaning once a String object is created, its character sequence can never be modified. This decision was not accidental — it was a deliberate trade-off that improved safety, simplified multithreading, and enabled powerful memory optimizations.
char[] arrays. Programmers can modify any character at any time, leading to both flexibility and frequent memory bugs.String class and introduces the String Pool — a shared memory area that reuses identical string literals to save heap space.StringBuilder, a mutable companion class designed for efficient string construction, addressing the performance cost of repeated concatenation with immutable Strings.char[] to a byte[] inside Strings, cutting memory use roughly in half for Latin-character text while preserving immutability.The central question this lesson addresses is: what exactly does it mean for a String to be immutable, and what are the real-world consequences — both beneficial and costly — of that design choice?
Core Principles & Definitions
Before diving into performance details, you need to understand several foundational ideas. In Java, every String is an object stored on the heap. When we say a String is immutable, we mean that the internal character data of that object can never be altered after the constructor finishes. No method on String — not toUpperCase(), not replace(), not concat() — modifies the original. Each of these methods returns a brand-new String object instead.
Immutability
String object is created, its internal character sequence is fixed forever. You cannot change a single character.String Pool
Reference vs. Object
Thread Safety
Hash Code Caching
hashCode() once and caches it. This speeds up hash-based structures like HashMap.Visual Explanation — Memory Under the Hood
The diagram below illustrates what happens in memory when you write seemingly simple String code. Notice that every operation that appears to "change" a String actually creates a new object, while the old one remains intact in the String Pool or on the heap.
s1 and s2 both point to the same pooled object at address 0x10. Concatenation does not alter "Hello" — it creates an entirely new String at 0x20.As you can see in the diagram, the key insight is the separation between references (the variables on the stack) and objects (the actual String data on the heap). When you reassign a String variable, you are simply changing which object the reference points to. The old object stays in memory, unchanged and eligible for garbage collection only when nothing references it anymore.
How Immutability Works Internally
Java enforces String immutability through several internal mechanisms. The String class is declared final, which means no subclass can override its methods to introduce mutation. Internally, the character data is stored in a private final byte[] field (since Java 9). The private modifier prevents outside access, and final ensures the array reference is never reassigned after construction. Crucially, the class never exposes this internal array directly — no getter returns the raw array — so external code cannot reach in and change a character.
The Three Pillars of String Immutability
- final class declaration — Prevents subclassing, so no one can create a MutableString that extends String.
- private final byte array — The character data is locked away inside the object and the reference itself cannot be swapped.
- No mutator methods — Every method that seems to modify a String (like
replace(),substring(),toLowerCase()) returns a new String object.
String Pool Mechanism
The String Pool (also called the String Intern Pool) is a special area in heap memory. When you write a string literal like "Hello", the JVM first checks the pool. If an identical String already exists, it returns a reference to the existing object instead of creating a new one. This optimization is only safe because Strings are immutable. If they could be changed, sharing objects between variables would be disastrous — modifying through one variable would silently change the value seen by every other variable pointing to the same object.
String s = new String("Hello") forces creation of a new object on the heap, bypassing the pool. This is almost always wasteful. Prefer the literal form String s = "Hello" to take advantage of pooling.Performance Implications — The Concatenation Problem
Immutability brings tremendous safety benefits, but it also introduces a well-known performance pitfall: repeated string concatenation. Every time you use the + operator to build up a String inside a loop, Java must create a new String object and copy all the characters into it. The old intermediate String becomes garbage. If you concatenate n times, each successive copy is larger, and the total work grows roughly proportional to n².
+ creates n separate String objects and copies characters quadratically. StringBuilder mutates a single buffer, keeping work linear.| n (loop iterations) | String + objects created | Total chars copied (String +) | StringBuilder objects | Total chars copied (SB) |
|---|---|---|---|---|
| 10 | 10 | 55 | 1 | 10 |
| 100 | 100 | 5,050 | 1 | 100 |
| 1,000 | 1,000 | 500,500 | 1 | 1,000 |
| 10,000 | 10,000 | 50,005,000 | 1 | 10,000 |
Worked Example — Tracing String Operations
Let's trace through a short program and count exactly how many String objects are created, which references point where, and whether two variables share the same object.
String a = "Java";
String b = "Java";
String c = a + "!";
String d = "Java!";
We need to determine how many distinct String objects exist in memory after all four lines execute, and which variables share objects."Java" is not yet in the String Pool, so the JVM creates a new String object containing "Java" and places it in the pool. Variable a stores a reference to this pooled object."Java" already exists in the pool, so no new object is created. Variable b receives a reference to the same object that a points to. This means a == b evaluates to true.+ always creates a new String object at runtime (unless both operands are compile-time constants). The literal "!" is created in the pool (1 new object), and the result "Java!" is created on the heap outside the pool (another new object). Variable c points to this heap object."Java!" is checked against the pool. It is not there (the "Java!" from line 3 was placed on the heap, not the pool), so a new String object "Java!" is created in the pool. Now c == d is false because they are different objects, but c.equals(d) is true because their contents match.String vs. StringBuilder vs. StringBuffer
Java provides three main classes for working with text. Understanding when to use each one is essential for writing code that is both correct and efficient.
| Feature | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread Safety | Yes (inherently) | No | Yes (synchronized) |
| Performance (single concat) | Fine | Fine | Fine |
| Performance (loop concat) | Poor — O(n²) | Excellent — O(n) | Good — O(n) but slower due to locking |
| String Pool | Yes (literals) | No | No |
| Best Use Case | Constants, keys, short-lived expressions | Building strings in loops (single thread) | Building strings in loops (multi-thread) |
String for everyday text that does not need to be assembled piece by piece. Switch to StringBuilder whenever you are building a string inside a loop — think of it like writing on a whiteboard you can erase and rewrite, instead of printing a new poster every time you want to add a word. StringBuffer is an older, thread-safe version of StringBuilder; in modern single-threaded code, StringBuilder is almost always preferred.Connection to Advanced Concepts
String immutability is not an isolated idea — it connects to several advanced topics you will encounter as you progress in computer science. The concept of immutability extends to many other areas of software design, from functional programming to data security.
| This Lesson | Advanced Topic |
|---|---|
| String Pool saves memory by reusing objects | Flyweight Design Pattern — a formal pattern for sharing objects to reduce memory, used in game engines, text editors, and more |
| Immutable Strings are inherently thread-safe | Concurrent Programming — immutable objects are a cornerstone of safe multithreaded code, eliminating race conditions without locks |
| hashCode() caching relies on immutability | Hash Tables & HashMap — understanding why mutable keys break hash maps connects directly to String immutability |
| StringBuilder uses a mutable internal buffer | Amortized Analysis — StringBuilder's dynamic array resizing strategy relates to the same amortized O(1) analysis used for ArrayList |
| Strings cannot be altered after creation | Security — passwords and class names stored as Strings cannot be changed by malicious code after validation, a key security feature in the JVM |
As you study data structures, algorithms, and software engineering, you will see immutability appear again and again. Languages like Kotlin, Scala, and Rust build immutability into their core design even more deeply than Java. Mastering the concept now gives you a solid foundation for understanding these advanced paradigms later.
Practice Problems
String s = "Hi"; s.toUpperCase(); will change s to "HI". Are they correct? Explain why or why not.
String x = "cat";
String y = "cat";
String z = new String("cat");
Also state whether x == y and x == z are true or false.String report = ""; for (String line : lines) { report = report + line + "\n"; } Identify the performance problem, estimate the order of magnitude of wasted work, and rewrite the code efficiently.Lesson Summary
Java Strings are immutable — once created, their character data can never change. This design is enforced by the final class declaration, a private final byte array, and the absence of any mutator methods. Immutability enables the String Pool (which saves memory by sharing identical literals), thread safety (no synchronization needed), and hash code caching (speeding up HashMaps).
The main performance trade-off is that repeated concatenation with + in a loop creates many temporary objects and copies characters quadratically — approximately n² ÷ 2 copy operations for n iterations. The solution is to use StringBuilder, which maintains a mutable internal buffer and performs the same work in linear time. Remember: methods like toUpperCase() and replace() return new Strings — they never modify the original. Always assign the return value if you want to keep the result.