Home/Learn/Java A–Z/Immutability

Immutability

Intermediate
OOP Deep Dive

Immutable 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.

ImmutableClass.java
// 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.

DefensiveCopy.java
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.

ImmutableRecord.java
// 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 Aria
1

What are the five rules for writing an immutable class?

MediumAmazon
2

Why is String a good HashMap key?

EasyGoogle
3

What is a defensive copy and when is it needed?

MediumOracle
4

Can an immutable class contain a mutable field?

MediumMicrosoft
5

How do Java records enforce immutability?

MediumNetflix

Ask Aria about Immutability

Your personal AI tutor — ask anything about this concept