SECTION 1
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.
1972 • C Language & Mutable Strings
char[] arrays. Programmers can modify any character at any time, leading to both flexibility and frequent memory bugs.1991 • Project Oak Begins
1995 • Java 1.0 Released
String class and introduces the String Pool — a shared memory area that reuses identical string literals to save heap space.2004 • Java 5 — StringBuilder Arrives
StringBuilder, a mutable companion class designed for efficient string construction, addressing the performance cost of repeated concatenation with immutable Strings.2017 • Java 9 — Compact 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?
SECTION 2
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.KEY TAKEAWAY
SECTION 3
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.
SECTION 4
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.
new String() vs. Literal
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.SECTION 5
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 |
Character copy counts follow the formula n × (n + 1) ÷ 2 for String concatenation.
STRING CONCATENATION COST
Where n is the number of concatenation operations. Each iteration i copies i characters into a new String, so the total work is the sum 1 + 2 + 3 + … + n.
SECTION 6
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.
How Many String Objects?
- 1
Step 1 — Analyze the Code
Consider this code: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. - 2
Step 2 — Line 1: String a = "Java"
The literal"Java"is not yet in the String Pool, so the JVM creates a new String object containing "Java" and places it in the pool. Variableastores a reference to this pooled object.Objects in memory: 1 ("Java" in pool) - 3
Step 3 — Line 2: String b = "Java"
The literal"Java"already exists in the pool, so no new object is created. Variablebreceives a reference to the same object thatapoints to. This meansa == bevaluates totrue.Objects in memory: still 1 — a and b share the same object - 4
Step 4 — Line 3: String c = a + "!"
Concatenation with+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). Variablecpoints to this heap object.Objects in memory: 3 — "Java" (pool), "!" (pool), "Java!" (heap) - 5
Step 5 — Line 4: String d = "Java!"
The literal"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. Nowc == disfalsebecause they are different objects, butc.equals(d)istruebecause their contents match.Total distinct String objects: 4 — "Java" (pool), "!" (pool), "Java!" (heap), "Java!" (pool)
SECTION 7
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) |
Comparison of Java's three text classes
WHEN TO USE WHAT
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.SECTION 8
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 |
How String immutability connects to advanced CS concepts
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
String s = "Hi"; s.toUpperCase(); will change s to "HI". Are they correct? Explain why or why not.Show answer
toUpperCase() does not modify the original String object. Instead, it creates and returns a brand-new String "HI". Since the code does not assign the return value to anything (such as s = s.toUpperCase()), the variable s still points to the original "Hi" object. The new "HI" object is immediately eligible for garbage collection.Question 2: BASIC CALCULATION
String x = "cat";
String y = "cat";
String z = new String("cat");
Also state whether x == y and x == z are true or false.Show answer
"cat" on line 1 creates one object in the String Pool. Line 2 reuses that same pooled object, so x == y is true. Line 3 uses new String(), which forces creation of a second, separate object on the heap (outside the pool). Therefore x == z is false, even though x.equals(z) is true because their contents are identical.Question 3: INTERMEDIATE
Show answer
StringBuilder. Create the StringBuilder before the loop, call sb.append(ch) inside the loop, and call sb.toString() after the loop. This approach copies only about 500 characters total — a 250× improvement.Question 4: APPLIED
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.Show answer
StringBuilder sb = new StringBuilder(); for (String line : lines) { sb.append(line).append("\n"); } String report = sb.toString(); This uses a single mutable buffer, copies each character roughly once, and produces only one final String object. The improvement is dramatic — from O(n²) to O(n) work.Question 5: CRITICAL THINKING
Show answer
a and b pointed to the same pooled object, then modifying the string through a would silently change the value seen through b — an extremely confusing and hard-to-debug bug called aliasing. The JVM could not safely share objects, so every literal would require its own copy, wasting significant memory. Beyond pooling, thread safety would also be lost: if one thread could mutate a shared String while another thread reads it, data races would occur. Additionally, cached hash codes would become invalid whenever the string changed, breaking HashMap lookups. Security would also suffer — validated class names or file paths stored as Strings could be altered by malicious code after the security check passes.SUMMARY
Lesson Summary
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.