Immutability
IntermediateImmutable objects cannot be changed after construction — they are inherently thread-safe, cacheable, and safe as HashMap keys.
Overview
An immutable object's state cannot change after construction. Immutability eliminates entire classes of bugs: no race conditions, no need for defensive copies by callers, safe HashMap keys, and free shareability across threads. The canonical examples are String, Integer, LocalDate, and records. Building a truly immutable class requires careful attention to final fields, defensive copies of mutable inputs, and not leaking mutable references.
Rules for Writing Immutable Classes
Five rules for a truly immutable class: 1. Declare the class final (or use a private constructor + factory). 2. Make all fields private and final. 3. Do not provide setters. 4. Defensively copy mutable inputs in the constructor. 5. Never return references to mutable internal state.
// Truly immutable class
public final class DateRange { // 1. final class
private final LocalDate start; // 2. private final
private final LocalDate end;
private final List<String> notes; // mutable field!
public DateRange(LocalDate start, LocalDate end, List<String> notes) {
if (start.isAfter(end))
throw new IllegalArgumentException("start must be before end");
this.start = start;
this.end = end;
this.notes = List.copyOf(notes); // 4. defensive copy → unmodifiable
}
public LocalDate getStart() { return start; } // 3. no setters
public LocalDate getEnd() { return end; }
public List<String> getNotes() {
return notes; // 5. safe — List.copyOf returned an unmodifiable list
}
// Wither method — returns new instance with one field changed
public DateRange withStart(LocalDate newStart) {
return new DateRange(newStart, end, notes);
}
}Defensive Copies and Mutable Leaks
A class that stores a reference to a mutable object passed in from outside can be mutated indirectly — even if it has no setters. The fix is a defensive copy in the constructor.
Similarly, returning a reference to internal mutable state leaks mutability. Return an unmodifiable view or a defensive copy.
import java.util.Date;
// BROKEN — Date is mutable, stored by reference
public final class BrokenEvent {
private final Date start;
public BrokenEvent(Date start) {
this.start = start; // stores reference!
}
public Date getStart() { return start; } // leaks mutable reference!
}
BrokenEvent event = new BrokenEvent(new Date());
Date d = event.getStart();
d.setTime(0); // mutates the "immutable" event!
// FIXED — defensive copies in and out
public final class SafeEvent {
private final Date start;
public SafeEvent(Date start) {
this.start = new Date(start.getTime()); // copy in
}
public Date getStart() {
return new Date(start.getTime()); // copy out
}
}
// Modern: use java.time — it's already immutable
public final class ModernEvent {
private final Instant start;
public ModernEvent(Instant start) { this.start = start; }
public Instant getStart() { return start; } // Instant is immutable — safe to return
}Records as Immutable Value Types
Java 14+ records are the easiest way to create immutable value types. The compiler generates constructor, getters, equals, hashCode, and toString. A compact constructor can add validation and defensive copies.
Records are shallowly immutable — if a component is a mutable type (List, Date), you must defensively copy in the compact constructor.
// Simple immutable record
record Point(double x, double y) {}
// Record with validation and defensive copy
record ImmutableList<T>(List<T> items) {
ImmutableList {
items = List.copyOf(items); // compact constructor — defensive copy
}
}
// Record with computed field
record Circle(Point center, double radius) {
// Compact constructor validates
Circle {
if (radius <= 0) throw new IllegalArgumentException("radius must be positive");
}
// Custom method on a record
public double area() {
return Math.PI * radius * radius;
}
public double circumference() {
return 2 * Math.PI * radius;
}
}
Circle c = new Circle(new Point(0, 0), 5.0);
System.out.println(c.area()); // 78.54
System.out.println(c.center()); // Point[x=0.0, y=0.0]
System.out.println(c); // Circle[center=Point[x=0.0, y=0.0], radius=5.0]Key Points to Remember
- Immutable classes: final class, private final fields, no setters, defensive copies in/out.
- Defensive copy in constructor prevents the caller from mutating internal state indirectly.
- Never return a mutable internal field reference — return a copy or unmodifiable view.
- java.time types (LocalDate, Instant) are immutable — prefer them over java.util.Date.
- Records are shallowly immutable — use List.copyOf() in compact constructors for mutable components.
Practice Immutability in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat are the five rules for writing an immutable class?
Why is String a good HashMap key?
What is a defensive copy and when is it needed?
Can an immutable class contain a mutable field?
How do Java records enforce immutability?
Ask Aria about Immutability
Your personal AI tutor — ask anything about this concept