Immutable objects
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
Moneyclass with a method that returns a new object. - Explain why a
finalfield 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
finalfield. - 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:
- 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.
- All fields
privateandfinal.privatekeeps outsiders away from them;finalmeans a field can be assigned exactly once, in the constructor, and never again. - The class cannot be subclassed. Declare the class
finalso that nobody creates a subclass that adds changing behavior. (The tutorial mentions a private constructor with factory methods as a more flexible alternative.) - 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:
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
}
}Main.java:10: error: cannot assign a value to final variable x
x = 5;
^
1 errorThat 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:
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:
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:
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);[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.
this.members = members.clone(); // copy in: the caller's array is now independent
return members.clone(); // copy out: the caller gets their own copyFor 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[]orint[], is safe after oneclone(). 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)
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:
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)));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)
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:
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);[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)
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 + ")"; }
}Point p = new Point(1, 2);
Point q = p.moveRight(3);
System.out.println(p + " " + q);(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.
class Position {
public int row;
public int col;
}Show solution B1
B1.
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.
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.
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:
finalclass,private finalfields, no setters. - Methods that "change" it return a new object.
finalonly 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
- What is an immutable class?
- Why copy a mutable list in the constructor?
- What does
finalon an array field guarantee, and what does it not? - How does an immutable
Moneyhandleplus? - Give one example of a type that should be immutable and one that should not.
Show answers
- A class whose objects cannot change after they are created: no setters,
private finalfields,finalclass. - Otherwise the caller keeps a reference and can change your supposedly immutable state.
- It guarantees the field always points to the same array; it does not stop the array's contents from changing.
- It returns a new
Moneyand leaves both operands unchanged. - 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
clonemethod 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.copyOfreturns 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.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.