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.
You should be comfortable with classes , the final keyword and access modifiers , and the Collections . Knowing records helps — they are immutable carriers by design.
💡 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.
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.
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.
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.
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 .
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.
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.