Immutability
An immutable object can't be changed after it's created: all its fields stay as the constructor set them. To "change" such an object, you create a new one. Immutable objects are easier to work with: you can safely pass them to any method or thread — nobody will spoil the data behind your back.
Immutable classes in Java
You've already used immutable objects. String, Integer and LocalDate don't change — their methods return new objects:
void main() {
String name = "anna";
String upper = name.toUpperCase();
IO.println(name);
IO.println(upper);
}Output:
anna
ANNAtoUpperCase() didn't change the string name; it returned a new one.
Your own immutable class
To make a class immutable, declare all its fields final (they can be assigned only in the constructor), write no setters, and have "modifying" methods return a new object:
final class Money {
private final int cents;
Money(int cents) {
this.cents = cents;
}
Money plus(Money other) {
return new Money(cents + other.cents);
}
@Override
public String toString() {
return "$" + cents / 100 + "." + String.format("%02d", cents % 100);
}
}
void main() {
Money price = new Money(1250);
Money total = price.plus(new Money(399));
IO.println(price);
IO.println(total);
}Output:
$12.50
$16.49The final on the class itself forbids inheritance: otherwise a subclass could add mutable fields.
Records
Records are immutable by default: their fields are automatically final, and there are no setters. It's the shortest way to create an immutable class. A "change" is done by creating a new record:
record Point(int x, int y) {
Point withX(int newX) {
return new Point(newX, y);
}
}
void main() {
Point start = new Point(1, 2);
Point moved = start.withX(10);
IO.println(start);
IO.println(moved);
}Output:
Point[x=1, y=2]
Point[x=10, y=2]Immutable collections
List.of, Set.of and Map.of create immutable collections. Any attempt to change them throws an exception. List.copyOf makes an immutable copy of an existing list:
void main() {
List<String> source = new ArrayList<>(List.of("tea", "coffee"));
List<String> copy = List.copyOf(source);
source.add("cocoa");
IO.println(source);
IO.println(copy);
}Output:
[tea, coffee, cocoa]
[tea, coffee]The copy didn't change along with the source list. There's also Collections.unmodifiableList(source) — but that's not a copy, it's a view: you can't change anything through it, yet changes to source show up in it.
A defensive copy
If an object receives a mutable list, a final field doesn't help: the list can still be changed from outside. That's why the constructor makes a defensive copy:
record Team(String name, List<String> members) {
Team {
members = List.copyOf(members);
}
}
void main() {
List<String> people = new ArrayList<>(List.of("Anna", "Oleg"));
Team team = new Team("Backend", people);
people.add("Ira");
IO.println(team.members());
}Output:
[Anna, Oleg]The record's compact constructor replaced the given list with an immutable copy. Adding Ira to the original list didn't affect the team.
Common mistake
final on a variable forbids assigning a different object to it, but doesn't stop the object itself from changing. A list declared final can be added to as much as you like, whereas a List.of list really can't be changed:
void main() {
final List<String> tasks = new ArrayList<>();
tasks.add("Buy bread");
IO.println(tasks);
List<String> fixed = List.of("Buy bread");
fixed.add("Call Oleg");
}Output:
[Buy bread]
Exception in thread "main" java.lang.UnsupportedOperationException
at Main.main(Main.java:7)The UnsupportedOperationException from List.of has no message — only the exception type is shown. Immutability is a property of the object, not of the variable: final protects only the reference.