Immutable classes in Java
final class, private final fields, no setters, defensive copies.
Never changes after birth
An immutable object's state can't change after construction. Any "change" builds a new object instead, just like String.toUpperCase(). Bonus: immutable objects are safe to share between threads with no locking at all.
The recipe
1. Declare the class **final. 2. Make fields private final. 3. Provide no setters. 4. Make defensive copies** of mutable data going in and out.
final class Money {
private final long cents;
Money(long cents) { this.cents = cents; }
Money plus(Money o) {
return new Money(cents + o.cents);
}
}final ≠ frozen
What does this print?
final var sb = new StringBuilder("go");
sb.append("al");
System.out.println(sb);gogoalCompile error
Show the answer
goal. final freezes the reference — sb can't point to another builder — but the builder object is still mutable.
Copy on the way in
The fields are private final, the getter returns a copy... but the constructor keeps the caller's own list. The caller can still modify it later, changing your "immutable" Team. Store a copy: this.names = List.copyOf(names);
final class Team {
private final List<String> names;
Team(List<String> names) {
this.names = names; // caller's list!
}
}A defensive copy at work
The constructor clones the array. What prints?
final class Range {
private final int[] v;
Range(int[] v) { this.v = v.clone(); }
int first() { return v[0]; }
}
void main() {
int[] data = {5, 6};
var r = new Range(data); data[0] = 0;
System.out.println(r.first());
}056
Show the answer
5. clone() gave Range its own array, so changing the caller's data afterwards doesn't affect it.
Why must the class be final?
Fields are private and final. Why also declare the class final?
Think about it, then reveal the answer
Otherwise someone could write a subclass that adds mutable state or overrides methods — and pass it anywhere your "immutable" type is expected. Making the class final (or its constructors private) closes that hole. Note: synchronized is not part of the recipe — immutable objects need no locks.
In real projects
String, Integer and all of java.time are immutable, which is why they make perfect HashMap keys and can be shared across threads freely. For plain data, Java 16 records give you private final fields, a constructor and accessors in one line.
Key takeaways
- Declare the class final so no subclass can add mutability
- Fields private final, no setters
- Copy mutable inputs in, and copies (or read-only views) out
- final on a reference doesn't freeze the object it points to
Records are only shallowly immutable: a record holding a List can still have that list changed from outside — unless its constructor stores List.copyOf(...).
Practice questions
Which step is NOT part of the usual recipe for an immutable class?
- Make every method synchronized
- Declare the class final
- Make fields private and final
- Provide no setter methods
Check your answer
Make every method synchronized. Immutable objects need no locking at all, since nothing can change. The other three steps are the core of the recipe.
What does this print?
final StringBuilder sb = new StringBuilder("a");
sb.append("b");
System.out.println(sb);- a
- ab
- Compile error
Check your answer
ab. final stops sb from pointing to a different object, but the StringBuilder it points to is still mutable.