Creating Immutable Classes in Java
Fast recall
Quick revision
Core answer
this during construction break the guarantee.Whether you know all five rules for immutability — especially defensive copying — not just 'use final'.
Thinking final fields are enough and forgetting defensive copies, so a getter hands out the internal List and callers mutate the 'immutable' object.
Note that final makes the reference unchangeable but not the object it points to, and that immutable objects need no synchronization across threads.
Interview-ready guide
Interview answer
I'd walk through it as a short recipe with one rule that really matters:
- Make the class
finalso no subclass can add mutable behavior. - Make every field
privateandfinal, so each gets assigned exactly once. - Set them only in the constructor, and provide no setters — now there is no way to change state after the object exists.
For plain values like a String or an int, that is already immutable.
The rule people forget is defensive copying, and it comes from a subtlety: final locks the reference, not the object it points to. A final List cannot be reassigned, but you can still call add on it. So if a field holds something mutable, you copy it on the way in — store List.copyOf of the caller's list, not the list itself — and you return an immutable snapshot or a defensive copy; a view of mutable backing data may still change on the way out, never the raw field. Skip that and a caller can mutate your supposedly immutable object from outside.
The reason this is worth it is thread safety for free: if state never changes, any number of threads can read the object with no locks and no data races. That is exactly why String, Integer, and the java.time types are immutable. And these days a record gives you the first four rules automatically — but if it holds a mutable field, you still write the defensive copy yourself.
Creating Immutable Classes in Java
Direct Answer
A common immutable-class design uses private final fields, initializes complete state during construction, exposes no mutating operations, prevents unsafe subclass mutation, and makes defensive snapshots of mutable inputs and outputs. A final reference alone is not enough. Properly constructed immutable state can be shared safely, but contained mutable objects and leaking this during construction break the guarantee.
Code Example
public final class ImmutableOrder { // Rule 1: final class
private final String orderId; // Rule 2: private final fields
private final List<String> items;
public ImmutableOrder(String orderId, List<String> items) {
if (orderId == null || orderId.isBlank())
throw new IllegalArgumentException("orderId required");
this.orderId = orderId;
this.items = List.copyOf(items); // Rule 5: defensive copy ON INPUT
} // Rule 3: constructor-only init
// Rule 4: no setters anywhere in this class
public String getOrderId() {
return orderId; // String is immutable — safe to return directly
}
public List<String> getItems() {
return items; // List.copyOf already returned unmodifiable list
}
}
// Usage:
List<String> input = new ArrayList<>(List.of("Laptop", "Mouse"));
ImmutableOrder order = new ImmutableOrder("ORD-001", input);
input.add("HACK"); // Does NOT affect the order
// order.getItems().add("X"); // Throws UnsupportedOperationException
System.out.println(order.getItems()); // [Laptop, Mouse]
A Java record gives you Rules 1–4 automatically. But if the record holds a mutable field like a List, you must still apply Rule 5 in the compact constructor.
Common Mistakes & Pitfalls
- Thinking
finalfields are enough —finalonly locks the reference; afinal Listcan still be changed with.add(), so the object is not truly immutable. - Storing the caller's mutable object directly —
this.items = itemslets the caller keep their reference and mutate your state later; copy it in the constructor withList.copyOf(items). - Returning the raw field from a getter — handing back the internal list lets callers mutate it from outside; return an unmodifiable view or a fresh copy.
- Forgetting to make the class
final— a subclass can add mutable fields or override methods and quietly break the immutability guarantee callers depend on.
Check your understanding
Answer these without looking back at the article.
- Why is String immutable in Java?
- Why are immutable objects thread-safe?
- What is a defensive copy and when do you need one?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.