Immutability
Definition
An immutable object never changes after it is created. To "change" it you create a new one. In Java this means final fields, no setters and defensive copies of anything mutable.
Why it exists
Shared objects that can change cause hard bugs, especially across threads. An object that never changes can be shared freely, cached and used as a map key with no locking.
How it works
Why it matters
- Thread safety: no mutable state means no races and no locks.
finalfields also get special visibility guarantees in the Java Memory Model (see Memory model). - Safe sharing: pass references freely, cache, use as
HashMapkeys with stable hash codes. - Failure atomicity: an object is never half-updated.
- Easier reasoning: no spooky action at a distance.
Cost: modifications create new objects (garbage). For small value objects this is cheap thanks to escape analysis and young-generation GC; for large structures use persistent data structures or builders.
Shallow vs deep
| Shallow immutable | Its own fields cannot change, but it may reference mutable objects |
| Deep immutable | Everything reachable is immutable |
Records are shallowly immutable:
record Team(String name, List<String> members) {
Team {
members = List.copyOf(members); // compact constructor makes it deeply immutable
}
}
Without the copy, team.members().add("x") would work on an ArrayList component.
Collections.unmodifiableList vs List.copyOf
List<String> source = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(source); // a VIEW: later changes to source show through
List<String> copy = List.copyOf(source); // an independent immutable copy
source.add("c");
view.size(); // 3
copy.size(); // 2
Use views to expose read-only access to your own internal collection; use copies to take ownership of input.
String immutability
String is immutable, which enables:
- String pool and interning: literals share one instance (
"a" == "a"is true;new String("a") == "a"is false).intern()returns the pooled instance (the pool lives on the heap since Java 7). - Cached hash code: computed once, so strings are ideal map keys.
- Security: file paths and class names cannot be changed after validation.
- Thread safety.
Pitfalls: building strings in loops with += creates many objects (the compiler optimises simple concatenation with invokedynamic, but not inside loops); use StringBuilder. Keep passwords in char[] so they can be wiped, since a String stays in memory until GC.
Example code
Recipe for an immutable class
- Make the class
final(or use a private constructor/sealed) so subclasses cannot add mutability. - Make all fields
private final. - No setters; "modifying" methods return new instances.
- Defensively copy mutable inputs in the constructor.
- Defensively copy or wrap mutable state on output.
- Do not let
thisescape from the constructor.
public final class Booking {
private final String id;
private final LocalDate start;
private final List<String> guests;
public Booking(String id, LocalDate start, List<String> guests) {
this.id = Objects.requireNonNull(id);
this.start = Objects.requireNonNull(start);
this.guests = List.copyOf(guests); // copy in: later changes to the caller's list do not affect us
}
public List<String> guests() { return guests; } // already immutable, safe to return
public Booking withStart(LocalDate newStart) { // "wither" returns a new instance
return new Booking(id, newStart, guests);
}
}
Date, arrays and StringBuilder are mutable: copy them (new Date(d.getTime()), array.clone()) or, better, use java.time types and immutable collections.
The classic leak
public final class Broken {
private final List<String> items;
public Broken(List<String> items) { this.items = items; } // stores the caller's reference
public List<String> items() { return items; } // hands it back too
}
List<String> list = new ArrayList<>(List.of("a"));
Broken b = new Broken(list);
list.add("b"); // b changed: it was never immutable
final makes the reference constant, not the object it points to.
Builders and "withers" for larger objects
public record Order(String id, List<Line> lines, Status status) {
public Order { lines = List.copyOf(lines); }
public Order withStatus(Status s) { return new Order(id, lines, s); }
}
For many optional fields, a builder collects mutable state, then build() produces the immutable object. Lombok's @With/@Builder or IDE tooling avoid boilerplate; records plus withX methods are sufficient for small types.
System design considerations
- Immutable messages and events are easy to serialise, replay and audit in distributed systems
- Copy-on-write structures give lock-free reads for read-mostly shared data
- Treat configuration as immutable snapshots that are swapped atomically
Safe publication
An object that is correctly constructed can still be seen partially constructed by another thread if it is shared through a data race. Immutable objects with all final fields are safe even then: the JMM guarantees a thread seeing the reference also sees the final fields as set in the constructor. For mutable objects, publish via volatile, final, a lock, or a concurrent collection.
class Holder {
private volatile Config config = Config.defaults(); // immutable Config swapped atomically
void reload() { config = Config.load(); } // readers see the old or the new, never a mix
Config current() { return config; }
}
An immutable snapshot behind a volatile (or AtomicReference) is a simple, powerful pattern for read-mostly configuration and routing tables.
Immutability and JPA / frameworks
JPA entities are inherently mutable (dirty checking, proxies, no-arg constructors); keep immutability for value objects, DTOs, events and configuration, and map between them. Use @Embeddable records carefully (support depends on the Hibernate version) and @Immutable for read-only entities.
When to use it, and when not
Use it when
- The object is a value (money, ids, configuration, DTOs)
- It is shared across threads or cached
- You want to use it safely as a map key or set element
Think twice when
- The object is large and changes often, where copying on every change is costly
- A framework needs a no-arg constructor and setters (some JPA entities)
- Mutability is the whole purpose, as with accumulators
Common mistakes
- Believing
finalmakes objects immutable. - Storing and returning caller-supplied collections or arrays directly.
- Records with mutable components and no copying.
- Letting
thisescape in a constructor (registering a listener, starting a thread). - Using
Collections.unmodifiableXwhen an independent copy was needed.
Interview questions
1. How do you create an immutable class?
Final class, private final fields, no setters, defensive copies of mutable inputs and outputs, and no escaping this. Return new instances for changes.
2. Is a class with a final List field immutable?
No. The reference is final, but the list's contents can change unless it is an immutable list copied from the input and never exposed in mutable form.
3. Are records immutable?
Shallowly. Component fields are final, but mutable components (lists, arrays, dates) stay mutable unless copied in the compact constructor.
4. Why is String immutable?
Thread safety, string pool sharing, cached hash codes, and security of values used as identifiers and paths.
5. String vs StringBuilder vs StringBuffer?
String is immutable; StringBuilder is a mutable, unsynchronised builder; StringBuffer is the synchronised legacy version (rarely needed).
6. How does final relate to the memory model?
After the constructor completes, other threads that obtain the reference see correctly initialised final fields, even without synchronisation (provided this did not escape).
7. When is immutability a poor fit?
Large, frequently updated structures where copying cost dominates (use mutable with confinement or persistent structures), and ORM entities.
Check yourself
1A class has all final fields but one field is a mutable List. Is the class immutable?
2Why can immutable objects be shared across threads without locks?
3Which is the best way to build an immutable class with many optional fields?
Key takeaways
- Immutability = no setters + defensive copies + final class + no escaping
this. finalis about references; deep immutability needs immutable components.- An immutable snapshot behind a volatile reference is a robust concurrency pattern.
- Use immutable value objects, mutable entities where frameworks demand them.
Finished this page? Track your progress (saved only in your browser).