Immutability & Defensive Copying
An immutable object is one whose state can never change after it is constructed, and defensive copying is how you stop mutable inputs and outputs from quietly breaking that guarantee.
Learn Immutability & Defensive Copying in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Before You Start
You should be comfortable with classes , the final keyword and access modifiers , and the Collections . Knowing records helps — they are immutable carriers by design.
What You'll Learn
The Big Idea — A Signed Contract
💡 Analogy: An immutable object is like a signed, witnessed contract . Once it's signed you can't scribble new terms onto it — if you want different terms you draft a whole new contract. That's why everyone can be handed a copy without worrying someone will alter it behind their back. Defensive copying is the lawyer photocopying any attachments you bring, so that even if you later edit your attachment, the signed version stays exactly as agreed.
Without that photocopy step, your "immutable" object is only as safe as the mutable list someone handed you — and they can keep editing it from outside.
1️⃣ The Recipe for an Immutable Class
For a class with only primitive or already-immutable fields, immutability is easy: make the class final , make every field private final , set them in the constructor, and provide no setters . Any "change" returns a brand-new object — exactly how String and BigDecimal work.
2️⃣ The Leak — Storing a Mutable Reference
The trouble starts when a field is mutable , like a List . If the constructor just stores the caller's list and the getter hands it straight back, your object isn't immutable at all — outside code can mutate it through the reference it still holds or through the getter.
3️⃣ Plugging the Leak — Defensive Copies
Copy on the way in and on the way out . List.copyOf(members) makes an independent, unmodifiable snapshot in the constructor, so later edits to the caller's list don't touch you. Because that copy is already read-only, the getter can return it directly — callers can read but never mutate.
4️⃣ Records — and Their Shallow Trap
A record is the natural home for immutable data — final fields, no setters, free equals / hashCode . But it is only shallowly immutable: a mutable component like a List can still be changed unless you defensively copy it in the compact constructor .
🧠 Quick Recall
Answer: no. final stops you reassigning the field to a different list, but you can still add / remove from the list it points to. Use List.copyOf for content immutability.
Answer: List.copyOf takes a true snapshot, so it is unaffected. Collections.unmodifiableList is just a view — if the underlying list changes, the view shows the change.
Answer: because no thread can change their state, there are no races to guard against — you can share one instance across threads with no locks at all.
🎯 YOUR TURN — Immutable Money
Give Money no setters and a plus that returns a new instance, leaving both operands unchanged.
🧩 MINI-CHALLENGE — Defensive Route
Build an immutable Route whose stops are defensively copied so neither the input list nor the accessor can alter it.
Common Errors
- ❌ Thinking final alone makes a class immutable. A final reference to a mutable list is still mutable. Fix: copy the contents with List.copyOf .
- ❌ Storing the caller's mutable argument directly. They keep a handle to your insides. Fix: defensively copy in the constructor.
- ❌ Returning the live collection from a getter. Callers can mutate it. Fix: return a copy or an unmodifiable view.
- ❌ Assuming a record is deeply immutable. Mutable components leak. Fix: copy them in the compact constructor.
- ❌ Adding a setter "just in case". One setter destroys immutability. Fix: return a new instance from a withX -style method instead.
- ❌ Confusing unmodifiableList with a copy. It's a live view. Fix: use List.copyOf when you need a true snapshot.
Pro Tips
- 💡 Default to immutable. Make things immutable unless you have a concrete reason not to — it removes whole classes of bugs.
- 💡 Use records for data carriers and copy mutable components in the compact constructor.
- 💡 Provide withX methods (e.g. withName(...) ) that return a modified copy, mirroring how the JDK's immutable types work.
- 💡 Immutable keys are safe keys. Since their hashCode never changes, they never get lost in a HashMap .
📋 Quick Reference
Concept
Syntax
Note
final class
final class T
Blocks subclassing
final field
private final int x;
Assign once
no setters
(omit setX)
State fixed at build
copy in
List.copyOf(arg)
Snapshot of input
copy out
return List.copyOf(field)
Or unmodifiable view
unmodifiable view
Collections.unmodifiableList(l)
Wraps, doesn't copy
array copy
arr.clone()
For mutable arrays
"change"
return new T(...)
New object, not mutation
record
record T(...)
Shallowly immutable
record copy
compact constructor
Copy mutable components
JDK examples
String, BigDecimal, LocalDate
Immutable by design
📚 Resources / Keep Learning
- 📘 List.copyOf — Java 21 API documentation (docs.oracle.com)
- 📘 String (an immutable class) — Java 21 API (docs.oracle.com)
- 📗 Records — dev.java tutorial
❓ Frequently Asked Questions
🎉 Lesson Complete!
You can now build a properly immutable class — final, no setters, "change" via new instances — spot the leak when a mutable field is stored or returned directly, plug it with defensive copies and List.copyOf , and harden a record's mutable components in its compact constructor.
Next up: The Builder Pattern — constructing complex immutable objects step by step without a telescoping constructor.
Practice quiz
What makes an object immutable?
- It has setters
- It uses var
- Its state cannot change after construction
- It is static
Answer: Its state cannot change after construction. An immutable object's observable state is fixed once the constructor finishes — there is no way to alter it afterwards.
Which keyword prevents reassigning a field after construction?
- final
- static
- const
- sealed
Answer: final. A final field can be assigned once (in the field initializer or constructor) and never reassigned.
Why take a defensive copy of a mutable constructor argument?
- For speed
- To save memory
- To enable setters
- So later edits to the caller's object don't change yours
Answer: So later edits to the caller's object don't change yours. If you store the same reference, the caller can mutate the shared object later and corrupt your 'immutable' state.
What does List.copyOf(list) return?
- The same list
- An unmodifiable copy
- A mutable copy
- null
Answer: An unmodifiable copy. List.copyOf returns an unmodifiable List containing the elements — attempts to mutate it throw UnsupportedOperationException.
Returning your internal mutable list directly from a getter...
- Leaks your state so callers can mutate it
- Is fine
- Improves performance only
- Is required
Answer: Leaks your state so callers can mutate it. Handing back the live collection lets callers add/remove elements, breaking immutability. Return a copy or an unmodifiable view.
Is a record automatically deeply immutable?
- Yes, always
- Only with final
- No — it's shallowly immutable; mutable components can still be changed
- Never immutable
Answer: No — it's shallowly immutable; mutable components can still be changed. A record's references are final, but if a component is a mutable list the contents can still change unless you defensively copy.
Where do you defensively copy a record's mutable component?
- In a setter
- In the compact constructor
- In toString
- You can't
Answer: In the compact constructor. A record's compact constructor is the place to validate and copy: 'Playlist { songs = List.copyOf(songs); }'.
A key benefit of immutability is...
- More setters
- Faster I/O
- Smaller class files
- Thread-safety with no locks
Answer: Thread-safety with no locks. Immutable objects can be shared freely across threads because no thread can change them — no synchronization needed.
How do you 'change' an immutable object?
- Mutate a field
- Return a new object with the change applied
- Use reflection
- You cannot do anything
Answer: Return a new object with the change applied. Immutable APIs return a new instance (like String.toUpperCase or BigDecimal.add) instead of mutating in place.
Which is a well-known immutable class in the JDK?
- ArrayList
- StringBuilder
- String
- HashMap
Answer: String. String is immutable; methods like substring and replace return new strings. BigInteger, BigDecimal and LocalDate are immutable too.
Continue this course
- Previous: equals(), hashCode() & Object Methods
- Next: The Builder Pattern