How to Create Immutable Classes in Java – Best Practices with Examples
An immutable class is a class whose instances cannot be changed after they are created. Once you construct an immutable object, its fields stay the same for its entire lifetime. String, Integer, BigDecimal, and LocalDate in the Java standard library are all immutable classes.
Immutable objects are inherently thread-safe, easier to reason about, and make excellent keys in HashMap and HashSet. This tutorial shows you exactly how to create one.
Rules for Making a Class Immutable
- Declare the class
final— prevents subclasses from overriding behavior - Declare all fields
private final— prevents direct access and reassignment - No setter methods — only provide getters
- Deep copy mutable fields in the constructor — prevent external mutation through shared references
- Return defensive copies of mutable fields from getters
Simple Immutable Class
package com.java9r;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* ImmutableEmployee – a thread-safe, immutable value object.
* Once created, no field can be changed.
*/
public final class ImmutableEmployee {
private final int id;
private final String name;
private final double salary;
private final List<String> skills; // mutable field – needs defensive copy
public ImmutableEmployee(int id, String name, double salary, List<String> skills) {
this.id = id;
this.name = name;
this.salary = salary;
// Defensive copy: don't store the caller's list reference
this.skills = Collections.unmodifiableList(new ArrayList<>(skills));
}
// Only getters – no setters
public int getId() { return id; }
public String getName() { return name; }
public double getSalary() { return salary; }
// Return unmodifiable view – caller cannot modify
public List<String> getSkills() { return skills; }
@Override
public String toString() {
return String.format("Employee{id=%d, name='%s', salary=%.2f, skills=%s}",
id, name, salary, skills);
}
}
Test the Immutable Class
package com.java9r;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> skillList = new ArrayList<>(Arrays.asList("Java", "Spring", "MySQL"));
ImmutableEmployee emp = new ImmutableEmployee(101, "Ravi Kumar", 75000.0, skillList);
System.out.println("Created: " + emp);
// Try to change the original list – should NOT affect emp
skillList.add("Hacking attempt");
System.out.println("After modifying original list: " + emp);
// skills list in emp is unchanged – defensive copy worked
// Try to modify the returned skills list – throws UnsupportedOperationException
try {
emp.getSkills().add("Another hack");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify returned skills: " + e.getClass().getSimpleName());
}
// Use as HashMap key – safe because hash never changes
var map = new java.util.HashMap<ImmutableEmployee, String>();
map.put(emp, "Engineering");
System.out.println("Department: " + map.get(emp));
}
}
Expected Output
Created: Employee{id=101, name='Ravi Kumar', salary=75000.00, skills=[Java, Spring, MySQL]}
After modifying original list: Employee{id=101, name='Ravi Kumar', salary=75000.00, skills=[Java, Spring, MySQL]}
Cannot modify returned skills: UnsupportedOperationException
Department: Engineering
Record Classes (Java 16+) — Immutability Made Easy
Java 16 introduced record classes that are immutable by default with minimal boilerplate:
// Java 16+ record – immutable, auto-generates constructor, getters, equals, hashCode, toString
record Point(double x, double y) {}
var origin = new Point(0.0, 0.0);
var p1 = new Point(3.0, 4.0);
System.out.println(p1.x()); // 3.0
System.out.println(p1.y()); // 4.0
// p1.x = 5.0; // compile error – records are immutable
Benefits of Immutable Classes
- Thread safety: No synchronization needed — multiple threads can read simultaneously
- Safe as Map/Set keys: Hash code never changes, so keys don't "disappear" from collections
- Easier debugging: State never changes after construction — no surprise mutations
- Free to share: No need to defensive-copy when passing to methods
Summary
To create an immutable class: mark it final, make all fields private final, initialize them only in the constructor, provide no setters, and use defensive copies for any mutable fields like lists, dates, or arrays. Immutable objects are the easiest path to thread-safe code and are a core pattern in functional and concurrent Java programming. In Java 16+, record types give you immutability automatically.
Comments