24 · 32 · 5 min
Objects that never change
An immutable object is one whose state cannot change after construction. That single restriction removes a surprising number of problems.
Building one
public final class Money {
private final long paise;
private final Currency currency;
public Money(long paise, Currency currency) {
this.paise = paise;
this.currency = Objects.requireNonNull(currency);
}
public Money plus(Money other) {
if (!currency.equals(other.currency))
throw new IllegalArgumentException("currency mismatch");
return new Money(paise + other.paise, currency); // a new object
}
}Four things make it hold: the class is final so nobody subclasses it into mutability, the fields are final and private, no method modifies state, and any mutable field would need defensive copying on the way in and out.
What it buys
Thread safety for free. A value that cannot change cannot be raced. No locks, no synchronisation, no memory visibility questions.
Safe sharing. Return it, cache it, pass it anywhere, with no defensive copy, because nobody can damage it.
Safe as a hash key. The hash cannot drift, which is exactly the failure from the collections page.
Easier reasoning. A value assigned once is a value you can trust for the rest of the method, which removes a whole category of "who changed this".
What it costs
A new object per change. For a loop appending to a string, that is quadratic behaviour, which is why StringBuilder exists alongside immutable String.
String s = "";
for (int i = 0; i < 100_000; i++) s += i; // copies the whole string each timeAllocation is cheap and generational collectors handle short lived objects well, so the cost is usually smaller than expected, and the mitigation when it is not is a mutable builder used locally and frozen at the end.
The practical rule
Default to immutable, and make something mutable when there is a reason.
| Make immutable | Keep mutable |
|---|---|
| Values: money, dates, coordinates | Large buffers built incrementally |
| Configuration | Long lived entities with identity |
| Anything shared across threads | Performance critical inner loops |
| Anything used as a key |
In other languages
Python's tuples, strings and frozensets are immutable; @dataclass(frozen=True) makes your own. Kotlin distinguishes val from var and listOf from mutableListOf. Rust makes immutability the default and requires mut to opt out, which is the strongest statement any mainstream language makes about it.