An immutable object's state can't change after construction. It is automatically thread-safe, safe to share and cache, and safe as a HashMap key — String, Integer and LocalDate are all immutable.
The recipe
Make the class
final(or make constructors private with static factories) so no subclass can add mutable state.
Make every field
private finalassigned once, in the constructor.
No setters
no method that changes a field. "Withers" return a new object:
withPrice(…).Defensive copy mutable inputs
copy lists/arrays/dates passed to the constructor, so the caller can't change them later.
Never expose mutable internals
return copies or unmodifiable views from getters.
A broken "immutable" class
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> tags = new ArrayList<>(List.of("java"));
Article a = new Article("Intro", tags);
tags.add("hacked"); // caller still holds the same list
a.tags().clear(); // getter hands out the internal list
a.tags().add("oops");
System.out.println(a.tags());
}
}
final class Article {
private final String title;
private final List<String> tags; // final reference, mutable list
Article(String title, List<String> tags) { this.title = title; this.tags = tags; }
List<String> tags() { return tags; }
}[oops]Fixed with defensive copies
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> tags = new ArrayList<>(List.of("java"));
Article a = new Article("Intro", tags);
tags.add("hacked"); // no effect on a
try {
a.tags().add("oops");
} catch (UnsupportedOperationException e) {
System.out.println("read-only");
}
Article b = a.withTitle("Basics"); // "modify" = new object
System.out.println(a.title() + " " + a.tags());
System.out.println(b.title() + " " + b.tags());
}
}
final class Article {
private final String title;
private final List<String> tags;
Article(String title, List<String> tags) {
this.title = Objects.requireNonNull(title);
this.tags = List.copyOf(tags); // copy in (and unmodifiable)
}
String title() { return title; }
List<String> tags() { return tags; } // safe: already unmodifiable
Article withTitle(String t) { return new Article(t, tags); }
}read-only
Intro [java]
Basics [java]Unmodifiable view vs immutable copy
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Integer> src = new ArrayList<>(List.of(1, 2));
List<Integer> view = Collections.unmodifiableList(src); // a read-only window
List<Integer> copy = List.copyOf(src); // an independent snapshot
src.add(3);
System.out.println(view); // sees the change!
System.out.println(copy); // doesn't
try {
List.of(1, null);
} catch (NullPointerException e) {
System.out.println("List.of rejects nulls");
}
}
}[1, 2, 3]
[1, 2]
List.of rejects nulls| API | Can you modify it? | Reflects source changes? | Nulls |
|---|---|---|---|
Collections.unmodifiableList(src) | No (UnsupportedOperationException) | Yes — it's a view | Allowed |
List.copyOf(src) / Set.copyOf / Map.copyOf | No | No — snapshot | Rejected |
List.of(…) / Set.of / Map.of | No | — | Rejected |
stream().toList() (16+) | No | No | Allowed |
Collectors.toList() | Yes (currently an ArrayList) | No | Allowed |
Why bother?
The cost is allocating a new object for every change. For heavy step-by-step construction, pair the immutable class with a mutable builder (like String + StringBuilder).
How do you make a class immutable?
Why is String immutable?
Collections.unmodifiableList vs List.copyOf?