Skip to content
Ameen Abozaid
PublishedBEG-021-L05

Immutable objects

  • Lesson 5 of 6
  • 12 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

So far we protected objects that change: a balance rises and falls, and the class makes sure it stays valid. This lesson takes the strongest form of protection: an object that never changes after it is created. You have already used one. In the strings module you saw that a String cannot be modified; operations like toUpperCase give you a new string.

In the bank picture, an immutable object is a printed banknote. You can spend it, count it, and pass it around, but nobody can alter the number printed on it. If you need a different amount, you get a different note.

Why this matters: values such as money, dates, coordinates, and identifiers are naturally unchanging. Making them immutable removes whole categories of bugs, because nobody can change a value you are holding.

This lesson builds on "Invariants" and "Getters, setters, and tell, don't ask", on arrays and strings from earlier modules, and on final fields (explained again here). It briefly previews List, which a later module teaches.

Learning Objectives

By the end of this lesson you will be able to:

  • Define an immutable class and name its four ingredients.
  • Write an immutable Money class with a method that returns a new object.
  • Explain why a final field is not enough when the field refers to something mutable.
  • Use defensive copies on the way in and on the way out.
  • Say what the compiler does when code tries to assign a final field.
  • State the benefits and the cost of immutability.

Written Explanation

What immutable means

An object is immutable when its state cannot change after construction. The Java tutorial on concurrency gives a short recipe for building one. In our own words and order:

  1. No setters. Provide no method that changes a field, or an object a field refers to. A method that would "change" the value returns a new object instead.
  2. All fields private and final. private keeps outsiders away from them; final means a field can be assigned exactly once, in the constructor, and never again.
  3. The class cannot be subclassed. Declare the class final so that nobody creates a subclass that adds changing behavior. (The tutorial mentions a private constructor with factory methods as a more flexible alternative.)
  4. Protect mutable references. If a field refers to a mutable object, never share that reference: copy it when it comes in, and return a copy when it goes out.

Each ingredient plugs one hole. Items 1 and 2 stop direct changes, item 3 stops changes by inheritance, and item 4 stops changes through a shared reference. We will see item 4 fail, then work.

The final keyword on fields

A final field is assigned once. The compiler enforces it:

java
final class Point {
    private final int x;
    private final int y;

    Point(int x, int y) { this.x = x; this.y = y; }

    void broken() {
        x = 5;   // attempt to change a final field
    }
}
text
Main.java:10: error: cannot assign a value to final variable x
        x = 5;
        ^
1 error

That is a compile-time guarantee, the same kind you got from private in lesson 1.

Methods that "change" return a new object

If a Point cannot move, how do you get a moved point? You build another one:

java
Point moveRight(int dx) { return new Point(x + dx, y); }

The original Point is untouched. Strings work exactly like that: "abc".toUpperCase() leaves "abc" as it was. Creating small new objects is cheap in Java, and the safety is worth it for small value types like money or coordinates.

Why final is not enough: the mutable field trap

final fixes the reference, not the thing referred to. A final array field always points to the same array, but the contents of that array can change.

Look at a Team class that looks immutable but is not:

java
class Team {
    private final String[] members;

    Team(String[] members) {
        this.members = members;          // keeps the caller's array
    }

    String[] members() {
        return members;                  // hands out the live array
    }
}

There are two leaks:

java
String[] names = {"Ana", "Omar"};
Team t = new Team(names);
names[0] = "HACKED";            // leak 1: the caller still holds the same array
t.members()[1] = "ALSO HACKED"; // leak 2: the getter handed out the same array
System.out.println(t);
text
[HACKED, ALSO HACKED]

(System.out.println(t) printed the array contents because Team defines a toString method that formats the array. The mechanism is taught in the module on the Object class; ignore it for now.)

Both lines change the "private" state from outside. Private and final did not help, because the same array object is shared between the caller and the team.

Defensive copies

The cure is a defensive copy: never store, or hand out, a reference to something the outside can still change.

java
this.members = members.clone();   // copy in: the caller's array is now independent
return members.clone();           // copy out: the caller gets their own copy

For arrays, clone() creates a new array of the same size with the same elements. The Java Language Specification says an array's clone is public and returns the array's own type, and that it makes a shallow copy: the new array holds the same element references. This matters for what you copy:

  • An array of immutable elements, such as String[] or int[], is safe after one clone(). Nobody can change the elements themselves.
  • An array of mutable objects needs each element copied too, or the elements stay shared.

The same ideas hold for collections like List, which a later module teaches. Preview only: List.copyOf(list) makes an unmodifiable copy for you, and the API says that later changes to the original list do not affect the copy. Do not try it yet unless you have met List.

Benefits and costs

Benefits.

  • Safe to share. Pass an immutable object anywhere; no one can change it under you.
  • Easy to reason about. The value you see at construction is the value forever.
  • Invariants checked once. If the constructor validates, the object stays valid.

Cost. Every "change" creates a new object. For small values this is cheap. For a large structure that changes thousands of times a second, immutability may not fit. A mutable class with strong encapsulation (like your Account) is also perfectly valid: things that have an identity and a life, such as an account or an order, usually change, whereas pure values, such as an amount or a date, usually do not.

Value or entity?

A helpful question: is this thing defined by what it is or by which one it is? Ten dollars is ten dollars; any ten-dollar note is as good as another, so Money should be an immutable value. Sara's account is this particular account whose balance changes over time, so Account is a changeable entity. Value types are the natural candidates for immutability.

Examples

Example 1: an immutable Money (simple)

java
final class Money {
    private final long cents;

    Money(long cents) {
        if (cents < 0) throw new IllegalArgumentException("Money cannot be negative: " + cents);
        this.cents = cents;
    }

    Money plus(Money other) {
        return new Money(cents + other.cents);
    }

    Money minus(Money other) {
        if (other.cents > cents) throw new IllegalArgumentException("Result would be negative");
        return new Money(cents - other.cents);
    }

    long cents() { return cents; }

    @Override
    public String toString() {
        return (cents / 100) + "." + String.format("%02d", cents % 100);
    }
}

Why whole cents in a long? Binary floating-point cannot represent most decimal fractions exactly, so many money systems store a whole number of the smallest unit. (This is a design choice made for this course; real projects may use other types.) The annotation @Override and the toString method are previews from the module on the Object class: they let println show 19.99 instead of an object address.

Using it:

java
Money price = new Money(1999);
Money tip = new Money(250);
Money total = price.plus(tip);
System.out.println(price + " + " + tip + " = " + total);
System.out.println("price is still " + price);
System.out.println(total.minus(new Money(5000)));
text
19.99 + 2.50 = 22.49
price is still 19.99
Exception in thread "main" java.lang.IllegalArgumentException: Result would be negative
	at Money.minus(Main.java:14)
	at Main.main(Main.java:33)

plus created a new Money; price was never touched. The constructor guards the invariant "never negative" and minus cannot slip past it, because it must go through the constructor.

Example 2: fixing the leaky Team (practical)

java
import java.util.Arrays;

final class Team {
    private final String[] members;

    Team(String[] members) {
        if (members == null || members.length == 0) {
            throw new IllegalArgumentException("A team needs at least one member");
        }
        this.members = members.clone();      // copy in
    }

    String[] members() {
        return members.clone();              // copy out
    }

    Team with(String newMember) {            // "change" = new object
        String[] bigger = Arrays.copyOf(members, members.length + 1);
        bigger[members.length] = newMember;
        return new Team(bigger);
    }

    @Override
    public String toString() { return Arrays.toString(members); }
}

Running the same attack as before, then adding a member:

java
String[] names = {"Ana", "Omar"};
Team t = new Team(names);
names[0] = "HACKED";
t.members()[1] = "ALSO HACKED";
System.out.println(t);
Team bigger = t.with("Lina");
System.out.println(t + " " + bigger);
text
[Ana, Omar]
[Ana, Omar] [Ana, Omar, Lina]

Both attacks changed only private copies. with returned a new Team and left the old one as it was. Arrays.copyOf is from the arrays utility lesson earlier in this course.

Example 3: a point that moves by returning a new point (realistic)

java
final class Point {
    private final int x;
    private final int y;

    Point(int x, int y) { this.x = x; this.y = y; }

    Point moveRight(int dx) { return new Point(x + dx, y); }

    @Override
    public String toString() { return "(" + x + ", " + y + ")"; }
}
java
Point p = new Point(1, 2);
Point q = p.moveRight(3);
System.out.println(p + " " + q);
text
(1, 2) (4, 2)

p is still (1, 2). Code holding p can rely on that, however many places also received it.

Backend Insight

Backend systems pass the same values through many layers: a price goes from the request to the service to the database to the response. If any layer could change the object, bugs would depend on who touched it last. Immutable value objects (money, identifiers, date ranges, email addresses) remove that class of bug, and you will meet them again as "value objects" in design work. Also common: configuration objects built once at startup and shared by every request, which must not change while requests are being served.

Returning internal collections directly from a method is one of the most common encapsulation leaks in real code reviews. Copy them out, or return a read-only view.

Common Mistakes

Mistake 1: Thinking final makes the whole object immutable

  • Incorrect: private final String[] members; is "immutable".
  • Correct: the reference cannot be reassigned, but the array contents can still change unless you copy in and out.

Mistake 2: Copying in but not out (or the reverse)

Both directions leak. Fix both, or the object stays changeable from outside.

Mistake 3: Adding a setter "just in case"

One setter ends immutability. If a "change" is needed, return a new object.

Mistake 4: Shallow copy of mutable elements

members.clone() copies the array but not what its elements refer to. For an array of mutable objects, copy each element too.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Name the four ingredients of an immutable class.

Show solution A1

A1. No setters; all fields private final; the class itself is final (so it cannot be subclassed); and defensive copies for any mutable field, on the way in and on the way out.

A2. What does the compiler do if a method of a class tries to assign to one of its final fields after the constructor?

Show solution A2

A2. It refuses to compile, with a message such as cannot assign a value to final variable x.

A3. Is String mutable or immutable? What does "abc".toUpperCase() do to the original?

Show solution A3

A3. Immutable. toUpperCase() returns a new string and leaves the original unchanged.

Level B: Application

B1. Make this class immutable and add a method moveDown(int dy) that returns a new object.

java
class Position {
    public int row;
    public int col;
}
Show solution B1

B1.

java
final class Position {
    private final int row;
    private final int col;

    Position(int row, int col) { this.row = row; this.col = col; }

    Position moveDown(int dy) { return new Position(row + dy, col); }

    int row() { return row; }
    int col() { return col; }
}

The fields are private and final, there are no setters, and moveDown returns a new object.

B2. A Playlist has private final String[] songs; with a constructor that stores its argument and a method that returns the array. Rewrite it with defensive copies. Show that changing the original array and changing the returned array no longer affects the playlist.

Show solution B2

B2.

java
import java.util.Arrays;

final class Playlist {
    private final String[] songs;

    Playlist(String[] songs) {
        this.songs = songs.clone();
    }

    String[] songs() {
        return songs.clone();
    }

    @Override
    public String toString() { return Arrays.toString(songs); }
}

With String[] in = {"A", "B"}; Playlist p = new Playlist(in); in[0] = "X"; p.songs()[1] = "Y"; System.out.println(p); the output is [A, B]. Both outside changes hit copies. (A production version would also reject a null argument.)

B3. Why is private final List<String> items; not enough to make a class immutable if the constructor stores the caller's list?

Show solution B3

B3. final only prevents reassigning the field. The list object is the caller's, who still holds a reference and can add or remove elements, and the class would also leak it if a getter returned it. The constructor should copy the list (for example with List.copyOf, once List is taught), and getters should not return the live list.

Level C: Challenge

C1. Add a method times(int quantity) to Money that returns the amount multiplied by a non-negative quantity. Keep the class immutable. What is new Money(250).times(3).cents()?

Show solution C1

C1.

java
Money times(int quantity) {
    if (quantity < 0) throw new IllegalArgumentException("Quantity cannot be negative: " + quantity);
    return new Money(cents * quantity);
}

new Money(250).times(3).cents() is 750.

C2. Decide whether each should be immutable, and give a one-line reason: (a) Temperature reading, (b) a bank Account, (c) a postal Address used on invoices.

Show solution C2

C2. (a) Immutable: a reading is a fact about one moment; a new reading is a new object. (b) Mutable: an account has an identity and its balance changes over time, protected by invariants. (c) Usually immutable: an address on a posted invoice should not change retroactively; a changed address is a new Address.

Mini Challenge

Design an immutable DateRange for a hotel booking with two whole-number fields startDay and endDay (days counted from some starting day, no date library needed). Requirements: the constructor rejects a start after the end; there are no setters; a method extendBy(int days) returns a new range; a method overlaps(DateRange other) returns a boolean.

Check your work: the class is final; both fields are private final; every method that "changes" returns a new object; the constructor is the only place the start-before-end rule is checked, and extendBy goes through it.

Summary & Next Lesson

Key takeaways

  • An immutable object cannot change after construction: final class, private final fields, no setters.
  • Methods that "change" it return a new object.
  • final only fixes the reference: a mutable field (array, list) needs a defensive copy on the way in and on the way out.
  • Immutable objects are safe to share and easy to reason about, at the cost of creating new objects.
  • Pure values (money, points, ranges) suit immutability; changing entities (accounts) suit encapsulated mutability.

Next lesson

You now have all the pieces. The next lesson is a demo, "Demo: protecting the balance invariant", which locks down Account step by step and prepares the module lab.

Self-check

  1. What is an immutable class?
  2. Why copy a mutable list in the constructor?
  3. What does final on an array field guarantee, and what does it not?
  4. How does an immutable Money handle plus?
  5. Give one example of a type that should be immutable and one that should not.
Show answers
  1. A class whose objects cannot change after they are created: no setters, private final fields, final class.
  2. Otherwise the caller keeps a reference and can change your supposedly immutable state.
  3. It guarantees the field always points to the same array; it does not stop the array's contents from changing.
  4. It returns a new Money and leaves both operands unchanged.
  5. Immutable: money, a point, or a date range. Mutable: an account or a shopping cart.

References

  • Oracle, A Strategy for Defining Immutable Objects. It supports the four-part recipe: no setters, private final fields, a final class, and protecting mutable references by copying in and out.
  • Oracle, Java Language Specification SE 21, chapter 10 (Arrays). Section 10.7 supports that an array's clone method is public, returns the array type, and makes a shallow copy.
  • Oracle, String (Java SE 21 API). It supports that strings are constant and cannot be changed after creation.
  • Oracle, List (Java SE 21 API). It supports that List.copyOf returns an unmodifiable snapshot, unaffected by later changes to the source.
  • The money-in-cents design and the remark that binary floating point cannot store most decimals exactly are stated from general knowledge and were not verified from a fetched page. All code and outputs come from compiling and running with JDK 21.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.