Immutable Classes
Creating immutable objects
6 min read
Immutable Classes in Java
An immutable class is one whose instances cannot be modified after creation. String is the most famous example. Immutability provides thread safety and eliminates side effects.
Rules for Immutability
- final class: Prevent subclasses from adding mutability
- private final fields: Cannot be changed after construction
- No setters: No way to modify state
- Defensive copies: For mutable objects passed in/out
- Don't allow this to escape: During construction
✓ Benefits: Thread-safe, cacheable, hashCode can be cached, no defensive copies needed, easier to reason about.
🔑 Java 16+: Use record for simple immutable classes - fields are final by default!
Code Examples
Simple immutable class pattern
java
1// Simple Immutable Class
2public final class ImmutablePerson {
3 private final String name;
4 private final int age;
5
6 public ImmutablePerson(String name, int age) {
7 this.name = name;
8 this.age = age;
9 }
10
11 // Only getters, no setters
12 public String getName() {
13 return name;
14 }
15
16 public int getAge() {
17 return age;
18 }
19
20 // To "modify", return new instance
21 public ImmutablePerson withAge(int newAge) {
22 return new ImmutablePerson(this.name, newAge);
23 }
24
25 public ImmutablePerson withName(String newName) {
26 return new ImmutablePerson(newName, this.age);
27 }
28}
29
30ImmutablePerson p1 = new ImmutablePerson("Alice", 25);
31ImmutablePerson p2 = p1.withAge(26); // New object!
32
33System.out.println(p1.getAge()); // 25 (unchanged)
34System.out.println(p2.getAge()); // 26 (new object)Defensive copies for mutable fields
java
1// Immutable with Mutable Fields - DEFENSIVE COPIES
2public final class ImmutableStudent {
3 private final String name;
4 private final List<String> courses; // List is mutable!
5 private final Date enrollmentDate; // Date is mutable!
6
7 public ImmutableStudent(String name, List<String> courses, Date date) {
8 this.name = name;
9 // Defensive copy on input
10 this.courses = new ArrayList<>(courses); // Copy!
11 this.enrollmentDate = new Date(date.getTime()); // Copy!
12 }
13
14 public String getName() {
15 return name; // String is immutable, OK
16 }
17
18 public List<String> getCourses() {
19 // Defensive copy on output
20 return new ArrayList<>(courses); // Copy!
21 // Or return unmodifiable view:
22 // return Collections.unmodifiableList(courses);
23 }
24
25 public Date getEnrollmentDate() {
26 // Defensive copy on output
27 return new Date(enrollmentDate.getTime()); // Copy!
28 }
29}
30
31// Without defensive copies, caller could modify internal state:
32List<String> courses = new ArrayList<>();
33courses.add("Java");
34ImmutableStudent s = new ImmutableStudent("Alice", courses, new Date());
35courses.add("Python"); // Would modify internal list without copy!Using immutable collections (Java 9+)
java
1// Using Immutable Collections (Java 9+)
2public final class ImmutableOrder {
3 private final String orderId;
4 private final List<String> items;
5 private final Map<String, Integer> quantities;
6
7 public ImmutableOrder(String orderId, List<String> items,
8 Map<String, Integer> quantities) {
9 this.orderId = orderId;
10 // Use immutable collections - no defensive copies needed!
11 this.items = List.copyOf(items);
12 this.quantities = Map.copyOf(quantities);
13 }
14
15 public String getOrderId() { return orderId; }
16
17 public List<String> getItems() {
18 return items; // Already immutable, safe to return
19 }
20
21 public Map<String, Integer> getQuantities() {
22 return quantities; // Already immutable
23 }
24}
25
26// Factory methods for immutable collections
27List<String> list = List.of("A", "B", "C");
28Set<String> set = Set.of("X", "Y", "Z");
29Map<String, Integer> map = Map.of("one", 1, "two", 2);
30
31// These throw UnsupportedOperationException on modification
32// list.add("D"); // ERROR!Java Records for easy immutability
java
1// Java Records (Java 16+) - Simplest Immutable Classes
2public record Person(String name, int age) {
3 // Automatically:
4 // - private final fields
5 // - Constructor
6 // - Getters (name() and age(), not getName())
7 // - equals(), hashCode(), toString()
8
9 // Can add validation in compact constructor
10 public Person {
11 if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
12 name = name.trim(); // Can modify before assignment
13 }
14
15 // Can add methods
16 public String greeting() {
17 return "Hello, " + name;
18 }
19}
20
21Person p = new Person("Alice", 25);
22System.out.println(p.name()); // Alice (not getName())
23System.out.println(p.age()); // 25
24System.out.println(p); // Person[name=Alice, age=25]
25
26// Records are final and fields are final - perfect immutability!
27
28// Note: For mutable field types, still need defensive copies
29public record Order(String id, List<String> items) {
30 public Order {
31 items = List.copyOf(items); // Make it immutable
32 }
33}Use Cases
- Thread-safe shared objects
- HashMap/HashSet keys
- Value objects and DTOs
- Configuration objects
- Safe sharing without copying
- Functional programming style
Common Mistakes to Avoid
- Forgetting defensive copies
- Not making class final
- Exposing mutable internals
- Forgetting to copy arrays
- Using Date instead of LocalDate
- Not handling collections properly