Command Palette

Search for a command to run...

PHASE 4Beginner ~28 min· topic 11 of 12

Topic 4.11

Immutable Objects

In one line

An immutable object can't change after it's constructed: every "change" returns a new object. Immutable objects are simple to reason about, safe to share between threads, and safe as HashMap keys, which is why String, Integer, LocalDate and records are built that way.

Think of it like this

A printed photograph versus a whiteboard. Anyone walking past a whiteboard can rub out and rewrite what's there, so you can never be sure it still says what you wrote. A photo stays exactly as it was printed. If you want a brighter version, you print a new photo, and the old one is untouched. Immutable objects are photographs; mutable ones are whiteboards.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Immutable
Can't be changed after it's created. To get a different value you make a new object.
Mutable
Can be changed after it's created, for example through setters or by adding items.
`final` field
A field that must be assigned exactly once, in the constructor or its declaration, and can never be reassigned.
Defensive copy
A copy made of a mutable object you receive or hand out, so others can't change your internal state through it.
Unmodifiable view
A wrapper that refuses changes made through it, but still shows changes made to the original underneath.
Wither
A method like withPrice(200) that returns a new object equal to this one except for one changed value.
Thread-safe
Works correctly even when several threads use it at the same time.

Step by step

01Mutable objects surprise you

A mutable Money object is passed to two orders as their price. Someone applies a discount to order A by calling price.setAmount(80). Order B's price changes too, because both orders share one object (Topic 3.3: the reference is copied, not the object).

Bugs like this are hard to find: the line that changed the value is far from the line that sees the wrong value. With immutable objects, sharing is always safe.

Mutable objects surprise youdiagram
Rendering diagram…

02Build an immutable class step by step

final class Money stops subclasses from adding setters. private final fields can be assigned only in the constructor. There are no setters. Operations like plus return a new Money.

Because Money can't change, you can share one instance anywhere, cache common values (Money.ZERO), and use it as a HashMap key with confidence.

Main.javawhole filejava
final class Money {
    private final long paise;
    private final String currency;

    Money(long paise, String currency) {
        this.paise = paise;
        this.currency = currency;
    }

    Money plus(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
        return new Money(paise + other.paise, currency);   // a new object
    }

    Money withPaise(long newPaise) {                       // a "wither"
        return new Money(newPaise, currency);
    }

    long paise() { return paise; }
}

03final is not immutable

private final List<String> tags; stops reassignment (tags = otherList won't compile) but tags.add("x") works fine. The reference is frozen; the list isn't.

So for each field ask two questions: can the field be reassigned? (fix with final) and can the object it points to change? (fix with an immutable type or a defensive copy).

04Defensive copies, in and out

In: if the constructor stores a caller's mutable object, the caller can change it later. Copy it: this.tags = List.copyOf(tags);. Copy *before* validating, so the caller can't change the value between your check and your copy (a time-of-check/time-of-use bug).

Out: if a getter returns a mutable internal object, the caller can change it. Return an immutable type (an unmodifiable list), or a copy.

Don't defensively copy immutable things (String, Integer, LocalDate, List.copyOf results). There's no need, and it wastes memory.

05View vs copy

Collections.unmodifiableList(original) returns a thin wrapper. Calling add on the wrapper throws UnsupportedOperationException, but changes to original show through the wrapper. It's read-only, not immutable.

List.copyOf(original) copies the elements into a new unmodifiable list. Later changes to original don't affect it. Note it rejects null elements with NullPointerException.

View vs copydiagram
Rendering diagram…

06Why immutable objects are thread-safe

Most concurrency bugs come from one thread changing something while another reads it. If nothing can change, there's nothing to race on and no lock needed.

There's one subtle requirement: the object must be safely constructed. JLS 17.5 guarantees that a thread that sees a reference to the object also sees the values the constructor wrote to its final fields, provided this didn't escape during construction. That's why the recipe forbids leaking this and why fields must be final, not just "never changed by convention".

Try it yourself

  1. 1

    Make the mutable price safe

    In the first example, give Order a copy of the price in its constructor (new MutableMoney(p.amount)). Predict the mutable: line. Which design is simpler, copying everywhere or making Money immutable?

  2. 2

    Swap view and copy

    In the second example, add tags.remove("java"); before the prints. Predict all three lines first.

  3. 3

    Check-then-copy bug

    In SafePeriod, move the validity check above the copies (checking the caller's start/end). Explain why that's weaker when another thread could change the caller's dates between the check and the copy.

Code & diagrams

Shared mutable price vs shared immutable price New tab
Sign in to run this example in your browser.

Expected output

mutable:   A=80 B=80
immutable: original=100 discounted=80
String: s=chai t=CHAI
final reference, unmodifiable view, true copy New tab
Sign in to run this example in your browser.

Expected output

tags: [java, records]
view: [java, records]
copy: [java]
view.add -> UnsupportedOperationException
copy.add -> UnsupportedOperationException
copyOf(copy) is same object: true
Defensive copies stop an attack on a Period New tab

java.util.Date is mutable, which is one reason java.time (immutable, Java 8) replaced it. Use Instant or LocalDate in new code.

Sign in to run this example in your browser.

Expected output

naive: 1000 -> 0 valid? false
safe:  1000 -> 5000 valid? true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Reassign a final field in a method

In a class with private final long balance = 0;, write a method void add() { balance = 5; }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:3: error: cannot assign a value to final variable balance
void add() { balance = 5; }
^
1 error

Break #2

Modify an immutable list

Write List<String> xs = List.of("a"); xs.add("b");.

terminal
$ java Main
── what you'll see ──
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.ImmutableCollections.uoe(ImmutableCollections.java:142)
at java.base/java.util.ImmutableCollections$AbstractImmutableCollection.add(ImmutableCollections.java:147)
at Main.main(Main.java:5)

Myth vs fact

Myth

Marking fields final makes a class immutable.

Fact

final stops reassignment of the field. If it points to a mutable object (a list, an array, a Date), that object can still change. You also need defensive copies and no mutating methods.

Myth

Collections.unmodifiableList makes an immutable list.

Fact

It's a read-only view. Anyone holding the original list can still change it, and the view shows those changes. List.copyOf makes an independent copy.

Myth

Immutable objects are slow because of all the copying.

Fact

Short-lived allocations are cheap on modern JVMs, and immutability removes the need for defensive copies when sharing and for locks across threads. Measure before optimising.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    JLS 17.5 (final field semantics): a thread that obtains a reference to an object after its constructor completes is guaranteed to see the correctly initialised values of its final fields, and of objects reachable through them as of the end of construction. Non-final fields get no such guarantee without synchronisation, which is how "effectively immutable" objects published through a data race can show default values.

  • ▸

    HotSpot doesn't trust final instance fields to be constant-folded by default, because reflection and deserialization can change them; it does trust static final fields, record fields, and fields of some JDK-internal classes (@Stable, @TrustFinalNonStaticFields). Work on "stable values" and "final means final" aims to change this in future releases.

  • ▸

    Immutable classes can cache derived values lazily without breaking immutability, as String does with its hash: the field isn't final, but every thread computes the same value, so a benign race is harmless. This trick only works for values that are deterministic and atomic to write (an int, not a long on old 32-bit JVMs).

  • ▸

    For big immutable objects that change often, look at persistent data structures (structural sharing, as in Vavr or Clojure collections) or a builder that produces the final immutable object once. String + StringBuilder is exactly this pairing (Topic 6.3).

Remember this

  1. 1

    An object is immutable if its observable state can never change after the constructor finishes. String is the famous example: s.toUpperCase() doesn't change s; it returns a new string. The same holds for Integer, BigDecimal, LocalDate, List.of(...) and records with immutable components.

  2. 2

    The recipe (Effective Java Item 17): (1) make all fields private final; (2) don't provide setters or any method that changes state; (3) make the class final (or use private constructors with factories) so subclasses can't add mutable state; (4) make defensive copies of mutable inputs in the constructor and of mutable outputs in getters; (5) don't let this escape from the constructor.

  3. 3

    final alone is not immutability. final List<String> items means the variable always points to the same list, but the list's contents can still change. final freezes the reference, not the object it points to.

  4. 4

    An unmodifiable view is not a copy. Collections.unmodifiableList(list) blocks changes made *through the view*, but if someone still holds the original list and changes it, the view shows the change. List.copyOf(list) (Java 10) makes a real, independent, unmodifiable copy (and returns the same object if it's already an unmodifiable List.of-style list).

  5. 5

    To change an immutable value you build a new one, usually through wither methods: price.withAmount(200) or date.plusDays(1). This costs an allocation, which modern JVMs handle cheaply, and it buys a lot: no defensive copies needed when sharing, no locks needed across threads, safe caching and safe use as map keys (Topic 4.8).

  6. 6

    Immutable objects are automatically thread-safe: if nothing can change, no thread can see a half-finished change. The Java Memory Model adds a special guarantee for final fields: once the constructor finishes (without leaking this), every thread sees their correct values, even without synchronisation (Phase 12).

Explain it without notes

01

List the rules for making a class immutable and explain the reason for each.

02

Why isn't final enough to make an object immutable?

03

What is the difference between Collections.unmodifiableList(list) and List.copyOf(list)?

04

Why are immutable objects thread-safe, and what condition must hold for that guarantee?

Practice

01

Write an immutable final class Temperature with a double celsius field, a plus(double delta) method returning a new object, and toString(). Show that the original is unchanged after plus.

02

Write an immutable final class Playlist holding a List<String> songs with a defensive copy in and an unmodifiable list out, plus withSong(String s) that returns a new playlist. Prove the original doesn't change.

03

Show that String methods return new objects: call concat, replace and trim on " hi " and print the original afterwards between brackets.

Trade-offs

  • ↔

    Immutable objects make code easier to reason about and safe to share, but each change allocates a new object. For a value updated millions of times in a tight loop (a counter, a buffer), a mutable object confined to one thread is the pragmatic choice.

  • ↔

    Defensive copying protects invariants but costs memory and time on every call. Using immutable component types (List.copyOf, String, java.time) removes the need to copy on the way out.

  • ↔

    Immutability by convention ("we just don't call the setters") gives none of the guarantees. Either enforce it with final fields and no mutators, or treat the class as mutable and document who may change it.

Done when you can

  • Done when you can write an immutable class following all five rules.

  • Done when you can explain why final isn't the same as immutable.

  • Done when you use defensive copies in and out, and copy before validating.

  • Done when you can explain unmodifiable view vs List.copyOf.

  • Done when you can explain why immutable objects are thread-safe and what safe construction means.