Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 5 – Classes, objects, records and enums

Immutable classes and defensive copies in Java

Build immutable Java classes with final fields and defensive copies, and see why final alone, unmodifiable views and == on values still leak.

  • Intermediate
  • 15 minutes
  • Examples run with Java temurin-25.0.4+101.0.LTS and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Design immutable classes with final fields, no setters and copies of every mutable part
  • Copy mutable inputs and outputs defensively, lists with List.copyOf and arrays with clone()
  • Distinguish an unmodifiable view from an unmodifiable copy, and choose between them
  • Explain why immutable objects are safe to share between threads, and the condition that guarantee needs
  • Compare value-based objects such as Integer with equals, and never use one as a lock

Before you start

What you’ll learn · Before you start

What you will learn

  • Design immutable classes with final fields, no setters and copies of every mutable part
  • Copy mutable inputs and outputs defensively, lists with List.copyOf and arrays with clone()
  • Distinguish an unmodifiable view from an unmodifiable copy, and choose between them
  • Explain why immutable objects are safe to share between threads, and the condition that guarantee needs
  • Compare value-based objects such as Integer with equals, and never use one as a lock

Before you start

On this page

LeakyTimetable has final fields and no setters, yet a timetable built with 2 stops printed 3 a moment later and 4 after that. An immutable object: An object whose state can never change once it exists, such as a String. Methods that seem to change it, like toUpperCase(), return a new object, so one object can be shared safely by many variables and threads. More in “Strings: first look and equality” is one whose state cannot change after its constructor has finished. String, Integer and LocalDate are immutable: methods that look like changes, such as toUpperCase() or plusDays(1), return a new object and leave the old one alone.

Immutable objects cannot surprise you. They are safe as keys in a HashMap (the previous lesson showed what a changing key does), they can be shared between threads without locks, and code that receives one never has to wonder who else might change it. This lesson builds such a class, shows in recorded runs how final alone, an unmodifiable view and == on value objects each let the guarantee slip, and ends with a class for you to fix.

final fixes the reference, not the object it refers to

A final field is assigned exactly once, by its initializer or in the constructor, and never again. The Java Language Specification is precise about what that does not cover: when a final variable holds a reference to an object, the state of the object may still change through its own methods, and only the variable stays pointed at the same object. The same holds for arrays. A final List<String> is a list reference that never moves to another list, while the list itself can grow, shrink and be cleared by anyone who holds it.

That gap is where most “immutable” classes leak. Here is a timetable with final fields and no setters that still changes after it was built, next to a version that does not:

Two timetables with final fields

Java (LeakyTimetable.java) · timetable/LeakyTimetable.java

import java.util.List;

/** Looks immutable: final fields and no setters. It still shares its list with every caller. */
final class LeakyTimetable {
    private final String train;
    private final List<String> stops;

    LeakyTimetable(String train, List<String> stops) {
        this.train = train;
        this.stops = stops;               // keeps the caller's list itself
    }

    List<String> stops() {
        return stops;                     // hands out the same list
    }

    @Override
    public String toString() {
        return train + " " + stops;
    }
}

Java (Timetable.java) · timetable/Timetable.java

import java.util.ArrayList;
import java.util.List;

/** Immutable: copies the list it is given and never hands out anything a caller can change. */
final class Timetable {
    private final String train;
    private final List<String> stops;

    Timetable(String train, List<String> stops) {
        this.train = train;
        this.stops = List.copyOf(stops);  // an unmodifiable copy, made before anyone else can see the object
    }

    List<String> stops() {
        return stops;                     // already unmodifiable, so it is safe to return as it is
    }

    /** "Changing" an immutable object means making a new one. */
    Timetable withStop(String stop) {
        List<String> longer = new ArrayList<>(stops);
        longer.add(stop);
        return new Timetable(train, longer);
    }

    @Override
    public String toString() {
        return train + " " + stops;
    }
}
What each timetable does when its list changes Java · timetable/Main.java
void main() {
    List<String> stops = new ArrayList<>(List.of("Pune", "Lonavala"));
    LeakyTimetable leaky = new LeakyTimetable("Morning Express", stops);
    Timetable safe = new Timetable("Morning Express", stops);

    stops.add("Karjat");                  // the caller changes its own list afterwards
    IO.println("after the caller's change:");
    IO.println("  leaky: " + leaky);
    IO.println("  safe:  " + safe);

    leaky.stops().add("Thane");           // anyone who gets the list can change the object
    IO.println("after stops().add on leaky: " + leaky);
    try {
        safe.stops().add("Thane");
    } catch (UnsupportedOperationException e) {
        IO.println("stops().add on safe: UnsupportedOperationException");
    }

    Timetable longer = safe.withStop("Mumbai");
    IO.println("withStop returns a new object: " + longer);
    IO.println("and safe is unchanged:         " + safe);
}

Output

after the caller's change:
  leaky: Morning Express [Pune, Lonavala, Karjat]
  safe:  Morning Express [Pune, Lonavala]
after stops().add on leaky: Morning Express [Pune, Lonavala, Karjat, Thane]
stops().add on safe: UnsupportedOperationException
withStop returns a new object: Morning Express [Pune, Lonavala, Mumbai]
and safe is unchanged:         Morning Express [Pune, Lonavala]

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

LeakyTimetable changes twice without a single setter. Step through the run and watch which list each reference leads to:

Interactive · step by step Which list each timetable holds Step through timetable/Main.java. The boxes on the left hold references; the ones on the right are the lists.
Which list each timetable holds, step 7 of 7. Showing stops, leaky's field, safe's field, the ArrayList (Pune … Karjat, Thane), unmodifiable copy, longer's field, a new copy. In focus: longer's field, a new copy.refers tothe same listits own copyrefers tostopsmain's variableleaky's fieldfinal Listsafe's fieldfinal Listthe ArrayListPune … Karjat, Thaneunmodifiable copyPune, Lonavalalonger's fieldfinal Lista new copyPune, Lonavala, Mumbai

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The caller's list. main builds an ArrayList holding Pune and Lonavala, and its variable stops refers to it.
  2. LeakyTimetable keeps it. The LeakyTimetable constructor assigns the argument to its final field. The field now refers to the caller's own list: one list, two references.
  3. Timetable copies it. The Timetable constructor stores List.copyOf(stops), a separate unmodifiable list, before any other code can see the new object.
  4. The caller adds Karjat. stops.add("Karjat") changes the ArrayList. leaky now prints three stops, because its field refers to that list; safe still prints two.
  5. A getter hands it out. leaky.stops() returns the field itself, so stops().add("Thane") changes the timetable again, without a setter: four stops.
  6. The copy refuses. safe.stops() returns the unmodifiable copy, so the same add throws UnsupportedOperationException and the copy still holds Pune and Lonavala.
  7. A change is a new object. safe.withStop("Mumbai") copies the stops into a new list, adds Mumbai and builds a new Timetable from it. The original timetable is exactly as it was.

First the caller adds Karjat to the list it passed to the constructor, and the timetable shows it, because its field holds that very list. Then any code that calls stops() gets the same list again and adds Thane. Timetable is unaffected by both, and withStop shows how an immutable class offers a change: it returns a new timetable and leaves the original exactly as it was. The diagram sums up the difference:

Without a copy the caller and the object share one list; with List.copyOf the object keeps its own unmodifiable list.Without a copyWith List.copyOfcaller'sstopsLeakyfieldone ArrayListPune, Lonavala,Karjatcaller'sstopsTimetablefieldArrayListPune, Lonavala,KarjatunmodifiablecopyPune, Lonavalaadds Karjatsees itadds Karjatunchangedcopy in theconstructor

A defensive copy gives the object a list of its own

Text description of the diagram

The diagram shows two cases, one above the other.

  1. Without a copy. The caller's variable stops and the field of LeakyTimetable both point to one ArrayList. When the caller adds Karjat to its list, the timetable sees Karjat too, because there is only one list.
  2. With List.copyOf. The caller's variable points to its own ArrayList, which now holds Pune, Lonavala and Karjat. The field of Timetable points to an unmodifiable copy made in the constructor, which still holds Pune and Lonavala: the caller's change does not reach it.

An arrow from the first case to the second is labelled "copy in the constructor".

Key idea

final stops a field from being pointed at another object; it does nothing to stop the list or array it points at from changing.

Checkpoint

Check: What final does and does not protect

  1. Question 1 of 2 What does this program print?

    Read the code, then choose one answer.

    final class Route {
        private final List<String> stops;
        Route(List<String> stops) { this.stops = stops; }
        int size() { return stops.size(); }
    }
    
    void main() {
        List<String> stops = new ArrayList<>(List.of("Pune", "Lonavala"));
        Route route = new Route(stops);
        stops.add("Karjat");
        IO.println(route.size());
    }
    Show the answer: Question 1 of 2

    Answer: 3, because the field refers to the caller's list, which grew

    final fixes which list the field refers to, not what is in the list. List.copyOf(stops) in the constructor would keep the size at 2.

  2. Question 2 of 2 How does Timetable let a caller add a stop?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: withStop returns a new Timetable with the extra stop and leaves the original as it was

    An immutable class offers a change the way String.toUpperCase does: it returns a new object and leaves the old one alone.

Five rules make a class immutable

Each rule closes a different gap, and the table says what goes wrong without it:

Rule Without it
1. Declare the class final A subclass adds a mutable field, or overrides stops() to return a list that can change
2. Make every field private and final Code can point a field at another object, and the compiler checks nothing
3. Write no setters; a “change” returns a new object Any caller can change the state, as with any ordinary class
4. Copy mutable inputs in the constructor The caller keeps a reference and changes the object later, as it added Karjat
5. Never hand out a mutable part A caller of a getter changes the object, as it added Thane

Rule 1 matters because code that accepts a Timetable cannot tell a subclass from the real thing: one subclass with a mutable field and the guarantee holds for some objects only. Rule 2 lets the compiler help, because final makes it check that each field is assigned exactly once, in every constructor. Rule 3 is why withStop exists.

Rules 4 and 5 are the defensive copy: A copy a class makes of a mutable object it receives or hands out, so that code outside the class can never change the class's state: List.copyOf(list) in a constructor, array.clone() in a getter. of this lesson’s title: List.copyOf(stops), array.clone() or the constructor of an immutable type on the way in, and an unmodifiable list, a copy of an array or an immutable value on the way out. They are defensive because they protect the object from code it cannot control: callers that keep a reference to what they passed in, and callers that change what they get back. Make the copy in the constructor, before anything else can see the object.

Checkpoint

Check: The five rules of an immutable class

  1. Question 1 of 2 Which of these does a class holding a List need, to be immutable?

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Declaring the class final
    • Copying the list given to the constructor, for example with List.copyOf
    • Never returning a list that callers can change

    An immutable class is final, keeps private final fields, has no setters, copies mutable inputs and never hands out a mutable part.

  2. Question 2 of 2 Rule 1 makes Timetable final. What could go wrong if it were not?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A subclass could add a mutable field or override stops(), and code that accepts a Timetable would get an object that can change

    Immutability is a promise about every object a variable of the type can refer to. A final class keeps subclasses from breaking that promise.

An unmodifiable view shows later changes, and a copy does not

The JDK offers two ways to make a list that nobody can change, and they behave differently when the original list changes later. Collections.unmodifiableList(list) returns an unmodifiable view: A wrapper such as Collections.unmodifiableList(list) that refuses every change made through it but reads through to the collection it wraps, so it still shows each change made to that collection.: its documentation says that queries on it “read through” to the list it wraps, and that only attempts to change the list through the view are refused. List.copyOf(list) returns an unmodifiable copy: a separate list that does not reflect later changes to the original.

A view, a copy and a mutable copy after the original changes Java · views/Main.java
// What each way of "protecting" a list does when the original list changes later.
void main() {
    List<String> original = new ArrayList<>(List.of("Pune", "Lonavala"));
    List<String> view = Collections.unmodifiableList(original);
    List<String> copy = List.copyOf(original);
    List<String> mutableCopy = new ArrayList<>(original);

    original.add("Karjat");
    IO.println("original:            " + original);
    IO.println("unmodifiableList:    " + view + "   (a view: it shows the change)");
    IO.println("List.copyOf:         " + copy + "           (a copy: it does not)");
    IO.println("new ArrayList<>(..): " + mutableCopy + "           (a copy anyone can change)");

    IO.println("view.add throws:     " + throwsOnAdd(view));
    IO.println("copy.add throws:     " + throwsOnAdd(copy));

    List<String> fixed = List.of("Pune", "Lonavala");
    IO.println("List.copyOf(List.of(...)) is the same object: " + (List.copyOf(fixed) == fixed));

    List<String> withNull = new ArrayList<>(Arrays.asList("Pune", null));
    IO.println("unmodifiableList keeps a null: " + Collections.unmodifiableList(withNull));
    try {
        List.copyOf(withNull);
    } catch (NullPointerException e) {
        IO.println("List.copyOf refuses a null:    NullPointerException");
    }
}

boolean throwsOnAdd(List<String> list) {
    try {
        list.add("Thane");
        return false;
    } catch (UnsupportedOperationException e) {
        return true;
    }
}

Output

original:            [Pune, Lonavala, Karjat]
unmodifiableList:    [Pune, Lonavala, Karjat]   (a view: it shows the change)
List.copyOf:         [Pune, Lonavala]           (a copy: it does not)
new ArrayList<>(..): [Pune, Lonavala]           (a copy anyone can change)
view.add throws:     true
copy.add throws:     true
List.copyOf(List.of(...)) is the same object: true
unmodifiableList keeps a null: [Pune, null]
List.copyOf refuses a null:    NullPointerException

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

In the table, “view” is Collections.unmodifiableList(list) and “copy” is List.copyOf(list); “throws” means an UnsupportedOperationException for changes and a NullPointerException for null elements.

A view and a copy of a list
Criterion viewcopy
Shows later changes YesNo
add, set, remove ThrowsThrows
null elements KeptThrows
A new list NoUsually
When to choose A read-only window on a list you ownFields and results of immutable classes

The run’s last copy, new ArrayList<>(list), is a copy too, but one that anyone holding it can change: right for a private working copy, wrong for a field of an immutable class.

So the view is right only when the class itself keeps changing the list and wants callers to watch, read-only. For an immutable class it is the wrong tool: whoever still holds the original can change what the view shows. List.copyOf is the right one, and it is cheap when it can be. Its documentation notes that copying a list that is already unmodifiable generally makes no copy at all, and the run shows List.copyOf(List.of(...)) returning the same object. Its one surprise is null: lists made by List.of and List.copyOf refuse null elements, so a constructor that copies with it also rejects a list that contains null, which is usually what you want anyway.

An unmodifiable list is still only as immutable as its elements. The List documentation warns that if the elements are mutable, the list’s contents can appear to change. A List<String> copy is fully safe, because String is immutable; a List<StringBuilder> copy is not.

Checkpoint

Check: Unmodifiable views and unmodifiable copies

  1. Question 1 of 2 view is Collections.unmodifiableList(original) and copy is List.copyOf(original). Then original.add("x") runs. Which statement is true?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: view now contains "x" and copy does not

    In the run, original and the view both print [Pune, Lonavala, Karjat], and the copy still prints [Pune, Lonavala].

  2. Question 2 of 2 What does List.copyOf do with a list that contains null?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It throws NullPointerException

    A constructor that copies with List.copyOf therefore rejects a list that contains null, which is usually what an immutable class wants anyway.

Arrays need clone() on the way in and on the way out

An array is always mutable, and final does not change that. The fix is the same two copies, with clone(), which for an array returns a new array of the same type holding the same elements:

arrays/Fares.java Java · arrays/Fares.java
import java.util.Arrays;

/** Fares in paise for each zone. An array is always mutable, so it is copied on the way in and on the way out. */
final class Fares {
    private final long[] paise;

    Fares(long[] paise) {
        this.paise = paise.clone();       // copy in
    }

    long[] paise() {
        return paise.clone();             // copy out: the caller gets an array of its own
    }

    long fareForZone(int zone) {
        return paise[zone];
    }

    @Override
    public String toString() {
        return "Fares" + Arrays.toString(paise);
    }
}
Changing the arrays on both sides Java · arrays/Main.java
void main() {
    long[] input = {1_000, 1_500, 2_500};
    Fares fares = new Fares(input);

    input[0] = 0;                         // changing the array that was passed in
    long[] out = fares.paise();
    out[1] = 0;                           // changing the array that came back
    IO.println(fares + ", zone 1 costs " + fares.fareForZone(1) + " paise");

    final long[] constant = {10, 20};
    constant[0] = 99;                     // final stops reassignment, not changes to the elements
    IO.println("a final array still changes: " + Arrays.toString(constant));
}

Output

Fares[1000, 1500, 2500], zone 1 costs 1500 paise
a final array still changes: [99, 20]

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java Main.java

The fares stay [1000, 1500, 2500] although the caller zeroed one element of the array it passed in and another of the array it got back. The last line shows the trap directly: a final array accepts constant[0] = 99.

clone() copies one level only. The documentation of Object.clone calls it a shallow copy: A copy that copies the references an array or object holds but not the objects they refer to, so the copy and the original share those objects. clone() on an int[][] copies only the outer array of row references. More in “2D and jagged arrays for grids and matrices”: for an array of objects, the new array points to the same objects. That is enough for arrays of primitives or of immutable values such as String. An array or list of mutable objects needs each element copied as well, or, better, immutable element types.

The same thinking applies to every mutable type a class keeps. The old java.util.Date is mutable, so a class that stores one has to copy it in and out; storing a LocalDate or an Instant from java.time instead removes the problem, because those classes are immutable.

Checkpoint

Check: Copying arrays and other mutable parts

  1. Question 1 of 2 In arrays/Main.java, the caller sets input[0] = 0 and out[1] = 0 after building the Fares. How many paise does fares.fareForZone(1) return?

    Type a number.

    A hint

    Ask which array each assignment changes.

    Show the answer: Question 1 of 2

    Answer: 1500 paise

    The constructor cloned input and the getter returned a clone, so neither change reaches the array inside Fares. Zone 1 still costs 1,500 paise, as the run prints.

  2. Question 2 of 2 A class stores a copy of a StringBuilder[] made with clone(). What does the caller's original array still share with the copy?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The StringBuilder objects themselves, so changing one shows in both arrays

    A shallow copy is enough for primitives and for immutable elements such as String. Mutable elements need copies of their own, or better, immutable element types.

Final fields make an immutable object safe to share between threads

Threads that share a mutable object need locks or other synchronization, or one thread can see another’s change half done. Immutable objects avoid the problem, and the Java memory model gives final fields a guarantee that plain fields do not have. Section 17.5 of the specification says that an object is completely initialized when its constructor finishes, and that a thread which can only see a reference to the object after that point is guaranteed to see the correct values of its final fields. It also sees the objects and arrays those final fields refer to at least as up to date as the fields themselves, which covers the copied list inside Timetable. Step through one timetable handed from one thread to another:

Interactive · message by message What a second thread is promised about final fields Step through one Timetable handed from a writer thread to a reader thread through a shared field.
What a second thread is promised about final fields, step 7 of 7. Messages so far: writer thread to shared field: store the reference; shared field to reader thread: the reference.writer threadshared fieldplain, not volatilereader threadstore the referencethe referencethis escapes: no guarantee

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The constructor runs. The writer calls new Timetable("Morning Express", stops). The constructor copies the list and assigns both final fields.
  2. The constructor finishes. The specification calls the object completely initialized at this moment. Nothing outside the constructor has seen it yet.
  3. The reference is shared. The writer stores the reference in a field another thread reads, with no lock and no volatile: a data race.
  4. The reader takes it. The reader thread reads the field and gets the reference to the timetable.
  5. The final fields are right. Because the reference was shared only after the constructor finished, the reader is guaranteed to see the right train and stops, and the copied list at least as up to date as the field.
  6. A plain field gets no promise. Had a field not been final, the same reader could see its default value, 0 or null, although the constructor had assigned it. The specification's own example shows such a 0.
  7. An escaping this. Had the constructor stored this somewhere other threads read, such as a static list of listeners, before it finished, a reader could see the object half built, final fields included.

The guarantee holds even when the reference reaches the other thread through a data race: Two accesses to the same variable from different threads, at least one of them a write, that nothing such as a lock or a volatile field puts in order. A thread that reads in a data race may see an old value, or a default such as 0 or null., and it comes with one condition: the constructor must not let this escape, for example by registering the object as a listener or storing it in a static field, before it finishes. A non-final field gets no such promise; another thread may see its default value of 0 or null. Timing-dependent behaviour like that cannot be shown reliably in a recorded run, which is exactly why the guarantee written in the specification matters: you can rely on it without having observed it.

Checkpoint

Check: Final fields and other threads

  1. Question 1 of 2 A Timetable constructor adds this to a static list of listeners before it assigns its stops. What does that cost?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The final-field guarantee, because another thread can find the object through the list before its fields are set

    JLS 17.5 promises correct final fields to a thread that can only see the object after its constructor finished. Publishing this earlier breaks the condition.

  2. Question 2 of 2 A thread receives, through a data race, a reference to an object whose int field is not final and was set to 5 in the constructor. What may it read?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Either 5 or the default value, 0

    This is the difference final makes: with a final field the reader is guaranteed 5, without it 0 is possible.

Value-based classes are compared with equals, not by identity

Many immutable classes of the JDK, among them Integer, LocalDate and Optional, are value-based class: A class, such as Integer, LocalDate or Optional, whose equal instances are meant to be interchangeable: code should compare them with equals, never rely on their identity with ==, and never use one as a lock. More in “Wrapper classes, autoboxing and caching”. Their documentation lists what that means: they are final, they have only final fields, equals, hashCode and toString depend only on their values, and they offer no constructor you should call (Integer’s are deprecated; you write Integer.valueOf(5) or simply 5). It adds two warnings. A program should not tell two equal instances apart by identity, for example with ==, and it should not synchronize on them, because identity-related behaviour may change in a future release.

== on boxed numbers, and a class that locks on an Integer

Java (Main.java) · valuebased/Main.java

void main() {
    Integer a = 127, b = 127;
    Integer c = 128, d = 128;
    IO.println("127 == 127:     " + (a == b));
    IO.println("128 == 128:     " + (c == d));
    IO.println("128 equals 128: " + c.equals(d));
}

Java (Counter.java) · valuebased/Counter.java

/** Synchronizes on an Integer: javac warns, because value-based objects have no identity you can own. */
final class Counter {
    private final Integer lock = 0;
    private int count;

    void increment() {
        synchronized (lock) {
            count++;
        }
    }
}
== on boxed numbers, and a lock that javac warns about Python · valuebased_session.py
from session import run

# The run, then javac on a class that locks on an Integer (a warning, not an error, so javac still succeeds).
run("""
java valuebased/Main.java
javac -d build valuebased/Counter.java
""")

Output

# openjdk version "25.0.4.1" LTS
$ java valuebased/Main.java
127 == 127:     true
128 == 128:     false
128 equals 128: true
$ javac -d build valuebased/Counter.java
valuebased/Counter.java:7: warning: [identity] attempt to synchronize on an instance of a value-based class
        synchronized (lock) {
        ^
1 warning

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 valuebased_session.py

127 == 127 is true and 128 == 128 is false, while equals is true for both. Integer.valueOf, which boxing uses, always caches the values from -128 to 127, so the same object comes back for them; outside that range the default settings create new objects. A comparison with == that works in a test with small numbers fails in production with larger ones. Compare values with equals.

javac reports the second warning itself. Locking on an Integer compiles, with an [identity] warning that the object is an instance of a value-based class: another part of the program could hold the same cached object and lock on it too. Lock on a private final Object lock = new Object() instead.

Comparing boxed numbers with ==

if (paid == due) with two Integer amounts passes every test with amounts up to 127 and fails for 128 and above, because == compares objects. Write paid.equals(due), or Objects.equals(paid, due) when either may be null.

Checkpoint

Check: Value-based classes, == and locks

  1. Question 1 of 2 Integer c = 128, d = 128; With the JVM's default settings, what are c == d and c.equals(d)?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: c == d is false and c.equals(d) is true

    The run printed 128 == 128 as false and 128 equals 128 as true. Integer is a value-based class: compare it with equals, never ==.

  2. Question 2 of 2 Why does javac warn about synchronized (lock) when lock is an Integer?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Integer is value-based, so another part of the program could hold the same cached object and lock on it too

    Lock on an object that only your class can reach, such as private final Object lock = new Object(), never on a value-based object.

Practise: make Order immutable

The checkout service builds Order objects and passes them around between threads. The class keeps the list and the array its callers give it and hands both out again. Make it immutable without changing its public API: the same constructor, the same three methods, the same results.

order/Order.java Java · order/Order.java
import java.util.List;

/**
 * An order as the checkout service builds it. Make it immutable without changing what callers can call:
 * the same constructor, the same three methods, the same results.
 */
public class Order {
    private String id;
    private List<String> items;
    private int[] quantities;

    public Order(String id, List<String> items, int[] quantities) {
        this.id = id;
        this.items = items;
        this.quantities = quantities;
    }

    public String id() {
        return id;
    }

    public List<String> items() {
        return items;
    }

    public int[] quantities() {
        return quantities;
    }
}
The sample checks against the starter class Java · order/OrderChecks.java
// Sample checks for Order. Edit Order.java, then run: java order/OrderChecks.java
int passed = 0;
int total = 0;

void check(String what, boolean ok) {
    total++;
    if (ok) passed++;
    IO.println((ok ? "pass  " : "FAIL  ") + what);
}

boolean refusesAdd(List<String> list) {
    try {
        list.add("extra");
        return false;
    } catch (UnsupportedOperationException e) {
        return true;
    }
}

void main() throws Exception {
    List<String> items = new ArrayList<>(List.of("kettle", "mug"));
    int[] quantities = {1, 2};
    Order order = new Order("A-1009", items, quantities);

    items.add("teapot");
    quantities[0] = 9;
    check("changing the caller's list does not change the order", order.items().equals(List.of("kettle", "mug")));
    check("changing the caller's array does not change the order", order.quantities()[0] == 1);

    check("items() cannot be changed through the returned list", refusesAdd(order.items()));
    order.quantities()[1] = 7;
    check("changing the returned array does not change the order", order.quantities()[1] == 2);

    check("the class is final", Modifier.isFinal(Order.class.getModifiers()));
    boolean allFinal = true;
    for (Field f : Order.class.getDeclaredFields()) allFinal &= Modifier.isFinal(f.getModifiers());
    check("every field is final", allFinal);
    check("the id is still A-1009", order.id().equals("A-1009"));

    IO.println(passed + " of " + total + " checks passed");
    if (passed < total) System.exit(1);
}

Output (exit status 1)

FAIL  changing the caller's list does not change the order
FAIL  changing the caller's array does not change the order
FAIL  items() cannot be changed through the returned list
FAIL  changing the returned array does not change the order
FAIL  the class is final
FAIL  every field is final
pass  the id is still A-1009
1 of 7 checks passed

Recorded with OpenJDK 25.0.4.1 LTS (Eclipse Temurin) on macOS 26 arm64. To run it yourself: java OrderChecks.java

Edit Order.java until java order/OrderChecks.java prints 7 of 7 checks passed:

  1. Make the class final and every field private final.
  2. In the constructor, keep List.copyOf(items) and quantities.clone() instead of the arguments themselves.
  3. Return the stored list as it is (it is unmodifiable now), and return quantities.clone() from quantities().

Then ask what the checks do not test: what happens if a caller passes a list that contains null, and is that the behaviour you want for an order?

Then work through questions on the six ideas, from final fields to value-based classes. Questions open where your checkpoint answers were weakest, and the reason behind every choice appears once you answer:

Practice that adapts to you

Practise this lesson

13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 13 What does final guarantee for a field of type List<String>?

    Choose one answer.

    Show the answer: Question 1 of 13

    Answer: After the constructor, the field always refers to the same list

    A final field is a reference that never moves to another object. Immutability needs that, plus a list nobody else can change.

  2. Question 2 of 13 In timetable/Main.java, how many stops does leaky print after leaky.stops().add("Thane")?

    Type a number.

    Show the answer: Question 2 of 13

    Answer: 4 stops

    Pune and Lonavala, then the caller's Karjat, then Thane added through the getter: four stops in the one list that leaky's field and the caller share.

  3. Question 3 of 13 A class has private final fields and no setters, keeps the List its constructor receives and returns it from a getter. Is it immutable?

    Choose one answer.

    Show the answer: Question 3 of 13

    Answer: No, because the caller who passed the list and every caller of the getter can change it

    LeakyTimetable is exactly this class: final fields, no setters, and it changed twice in the run.

  4. Question 4 of 13 A Course class receives a List<String> of topics and must stay immutable. Where should the copy go?

    Choose one answer.

    Show the answer: Question 4 of 13

    Answer: In the constructor, with List.copyOf; the getter can then return the field as it is

    Copy once on the way in, with a copy that is unmodifiable, and the way out needs nothing more, as in Timetable.

  5. Question 5 of 13 What happens when code calls add on Collections.unmodifiableList(list)?

    Choose one answer.

    Show the answer: Question 5 of 13

    Answer: It throws UnsupportedOperationException

    Unmodifiable views refuse changes made through them and still show changes made to the list they wrap.

  6. Question 6 of 13 What does List.copyOf(fixed) return when fixed was made with List.of("Pune", "Lonavala")?

    Choose one answer.

    Show the answer: Question 6 of 13

    Answer: The same list object, because it is already unmodifiable

    That is why List.copyOf is cheap when it can be: an immutable class can copy its inputs every time without paying for lists that are already unmodifiable.

  7. Question 7 of 13 A class keeps a mutable list of today's orders and wants callers to watch it change, read-only. What should its getter return?

    Choose one answer.

    Show the answer: Question 7 of 13

    Answer: Collections.unmodifiableList(orders)

    A view is right when the class keeps changing the list and wants callers to watch it read-only; a copy is right for the fields and results of immutable classes.

  8. Question 8 of 13 What does clone() return for a long[]?

    Choose one answer.

    Show the answer: Question 8 of 13

    Answer: A new array of the same type, holding the same elements

    An array is always mutable. clone gives a separate array, which is enough for primitives and immutable elements.

  9. Question 9 of 13 An immutable Booking stores a java.util.Date for the day of travel. What is the simplest safe design?

    Choose one answer.

    Show the answer: Question 9 of 13

    Answer: Store a LocalDate instead, which is immutable

    Date is mutable, so a class that stores one copies it in and out. java.time types such as LocalDate and Instant are immutable, so storing them needs neither copy.

  10. Question 10 of 13 Why can an immutable object be shared between threads without locks?

    Choose one answer.

    Show the answer: Question 10 of 13

    Answer: Nobody can change it, and its final fields are seen correctly by every thread once the constructor has finished

    Locks protect changes. An object that never changes, with final fields, gives every thread the same correct values without them.

  11. Question 11 of 13 The final-field guarantee of JLS 17.5 comes with one condition. What is it?

    Choose one answer.

    Show the answer: Question 11 of 13

    Answer: The constructor must not let this escape before it finishes

    Set the final fields in the constructor and do not publish this before the constructor finishes: that is the whole usage model the specification gives.

  12. Question 12 of 13 Integer a = 127, b = 127; With the default settings, what is a == b?

    Choose one answer.

    Show the answer: Question 12 of 13

    Answer: true, because Integer.valueOf always caches -128 to 127

    127 == 127 is true and 128 == 128 is false in the run. A test with small numbers passes, and production with larger ones fails: compare with equals.

  13. Question 13 of 13 An Order's equals compares two Integer fields that hold amounts, some above 127, and either may be null. How should it compare them?

    Choose one answer.

    Show the answer: Question 13 of 13

    Answer: Objects.equals(amount, other.amount)

    Value-based classes are compared with equals. Objects.equals is the safe form when a field may be null.

Interview questions

Fresher · What makes a Java class immutable?

Its state cannot change after the constructor finishes, and that takes five things. The class is final, so no subclass can add mutable state or override its methods. Every field is private and final. There are no setters, and methods that look like changes return a new object, as String.toUpperCase() does. Mutable inputs, such as a list or an array passed to the constructor, are copied before they are stored, with List.copyOf or clone(). And mutable parts are never handed out: getters return an unmodifiable list or a copy. The point a good answer makes is that final alone is not enough, because a final field can still refer to a list that someone else changes. Immutable objects are then safe as map keys and safe to share between threads, since final fields are guaranteed to be seen correctly once the constructor has finished.

Two longer questions of this module start from this lesson: a configuration class that its author calls immutable, and a price table that one thread replaces every minute while others read it.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    In a code review, a Config class has private final Map<String, String> settings, assigned from the constructor argument, and a getSettings() that returns the field. The author says it is immutable because the field is final and there are no setters. Do you agree? What would you change, and what does each change cost?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A pricing service builds a new immutable PriceTable every minute and stores it in a plain static field; request threads read the field without locks. What exactly does the Java memory model guarantee the readers, what does it not guarantee, and what would you change?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An immutable object cannot change after its constructor finishes; “changing” it means making a new object, as withStop does.
  • final stops a field from being reassigned, and nothing more: the list, array or other object it refers to can still change.
  • The recipe: a final class, private final fields, no setters, copies of mutable inputs, and no mutable parts handed out; arrays are copied with clone(), which is shallow, so prefer immutable elements.
  • Collections.unmodifiableList is a view that shows later changes to the original; List.copyOf is an unmodifiable copy that does not, and it rejects null elements.
  • Final fields of an object whose constructor did not let this escape are seen correctly by every thread, which is why immutable objects can be shared without locks.
  • Value-based classes such as Integer and LocalDate are compared with equals, never ==, and never used as locks: two boxed Integer objects holding 128 are not ==.

Terms in this lesson

data race
Two accesses to the same variable from different threads, at least one of them a write, that nothing such as a lock or a volatile field puts in order. A thread that reads in a data race may see an old value, or a default such as 0 or null.
defensive copy
A copy a class makes of a mutable object it receives or hands out, so that code outside the class can never change the class's state: List.copyOf(list) in a constructor, array.clone() in a getter.
unmodifiable view
A wrapper such as Collections.unmodifiableList(list) that refuses every change made through it but reads through to the collection it wraps, so it still shows each change made to that collection.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.