Java • Syntax and Core Types

String Immutability — Explain String immutability and performance implications (conceptual)

Discover why Java Strings can never change and how that design choice shapes the performance of your programs.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 6 figures

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

Dennis Ritchie creates C, which represents strings as raw char[] arrays. Programmers can modify any character at any time, leading to both flexibility and frequent memory bugs.

1991 • Project Oak Begins

James Gosling starts the Oak project (later renamed Java), explicitly targeting safety and simplicity. Immutable strings become a core design principle from day one.

1995 • Java 1.0 Released

Java ships with an immutable 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

Java 5 adds 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

Java 9 internally switches from a 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.

1

Immutability

Once a String object is created, its internal character sequence is fixed forever. You cannot change a single character.
2

String Pool

Java maintains a special memory region called the String Pool where identical string literals are stored only once, saving memory.
3

Reference vs. Object

A String variable holds a reference (like an address) that points to the actual object. Reassigning the variable changes the reference, not the object.
4

Thread Safety

Because no thread can alter a String's contents, immutable Strings are automatically thread-safe — multiple threads can read the same String without conflicts.
5

Hash Code Caching

Since the contents never change, Java computes a String's hashCode() once and caches it. This speeds up hash-based structures like HashMap.
✦

KEY TAKEAWAY

Think of a String like a printed page in a book. You can photocopy it, highlight the copy, or write on the copy, but the original page stays exactly as it was when it was printed. Every time you "modify" a String in Java, you are really creating a new photocopy — a brand-new object in memory — while the original remains untouched.

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.

String Immutability in MemoryStack (Variables)s1ref → 0x10s2ref → 0x10s3ref → 0x20Heap — String Pool0x10"Hello"0x20"Hello World"Code ExecutionString s1 = "Hello";→ creates "Hello" in pool (0x10)String s2 = "Hello";→ reuses same object (0x10) — no new object!String s3 = s1 + " World";→ creates NEW object "Hello World" (0x20)// "Hello" at 0x10 is unchanged!→ original String is never modified
Variables 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

  1. final class declaration — Prevents subclassing, so no one can create a MutableString that extends String.
  2. private final byte array — The character data is locked away inside the object and the reference itself cannot be swapped.
  3. 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

Writing 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².

String Concatenation vs. StringBuilder — Object CreationBuilding the string "ABCDE" one character at a timeUsing String + (Immutable)"A"→"AB"→"ABC"→"ABCD"→"ABCDE"5 String objects created — 4 thrown away as garbageCharacters copied: 1 + 2 + 3 + 4 + 5 = 15 total copy operationsUsing StringBuilder (Mutable)ABCDE(unused capacity)1 object used — characters appended in placeCharacters copied: 1 + 1 + 1 + 1 + 1 = 5 total append operationsGrowth Comparison for n Concatenationsworkn (iterations)String + : ~n²StringBuilder : ~n
Repeatedly concatenating with + creates n separate String objects and copies characters quadratically. StringBuilder mutates a single buffer, keeping work linear.
n (loop iterations)String + objects createdTotal chars copied (String +)StringBuilder objectsTotal chars copied (SB)
101055110
1001005,0501100
1,0001,000500,50011,000
10,00010,00050,005,000110,000

Character copy counts follow the formula n × (n + 1) ÷ 2 for String concatenation.

STRING CONCATENATION COST

Totalcharacterscopied=n×(n+1)÷2≈n2/2Total characters copied = n × (n + 1) ÷ 2 ≈ n² / 2

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. 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. 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. Variable a stores a reference to this pooled object.
    Objects in memory: 1 ("Java" in pool)
  3. 3

    Step 3 — Line 2: String b = "Java"

    The literal "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.
    Objects in memory: still 1 — a and b share the same object
  4. 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). Variable c points to this heap object.
    Objects in memory: 3 — "Java" (pool), "!" (pool), "Java!" (heap)
  5. 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. Now c == d is false because they are different objects, but c.equals(d) is true because 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.

FeatureStringStringBuilderStringBuffer
MutabilityImmutableMutableMutable
Thread SafetyYes (inherently)NoYes (synchronized)
Performance (single concat)FineFineFine
Performance (loop concat)Poor — O(n²)Excellent — O(n)Good — O(n) but slower due to locking
String PoolYes (literals)NoNo
Best Use CaseConstants, keys, short-lived expressionsBuilding strings in loops (single thread)Building strings in loops (multi-thread)

Comparison of Java's three text classes

💡

WHEN TO USE WHAT

Use 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 LessonAdvanced Topic
String Pool saves memory by reusing objectsFlyweight Design Pattern — a formal pattern for sharing objects to reduce memory, used in game engines, text editors, and more
Immutable Strings are inherently thread-safeConcurrent Programming — immutable objects are a cornerstone of safe multithreaded code, eliminating race conditions without locks
hashCode() caching relies on immutabilityHash Tables & HashMap — understanding why mutable keys break hash maps connects directly to String immutability
StringBuilder uses a mutable internal bufferAmortized Analysis — StringBuilder's dynamic array resizing strategy relates to the same amortized O(1) analysis used for ArrayList
Strings cannot be altered after creationSecurity — 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

A classmate claims that String s = "Hi"; s.toUpperCase(); will change s to "HI". Are they correct? Explain why or why not.
Show answer
They are incorrect. Because Strings are immutable, the method 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

How many distinct String objects exist in memory after this code runs? 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
There are 2 distinct String objects. The literal "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

A program concatenates a single character to a String inside a loop that runs 500 times. Using the formula for String concatenation cost, calculate approximately how many total character-copy operations occur. Then explain how you would rewrite the code to avoid this waste.
Show answer
Using the formula n × (n + 1) ÷ 2, with n = 500, the total character copies are 500 × 501 ÷ 2 = 125,250. That is over 125 thousand copy operations just to build a 500-character String! To fix this, replace the String concatenation loop with a 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

You are writing a method that reads 10,000 lines from a file and builds a single large report string by appending each line with a newline character. Your teammate writes: 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
The problem is that each iteration of the loop creates a new String object containing all previous lines plus the new one. With 10,000 lines, the total characters copied approaches 10,000 × 10,001 ÷ 2 ≈ 50 million copy operations, and 10,000 temporary String objects are created and discarded. The fix is: 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

If Java Strings were mutable (like character arrays in C), the String Pool optimization would be impossible. Explain in your own words why sharing pooled objects between multiple variables would be dangerous with mutable strings. Then describe at least one other benefit of immutability (besides pooling) that would also be lost.
Show answer
If Strings were mutable and two variables like 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.

Varsity Tutors • Java • String Immutability — Explain String immutability and performance implications (conceptual)