Defensive Copying and Immutability¶
Defensive copying establishes ownership by preventing a caller and callee from mutating the same state unexpectedly. Immutability goes further: after construction, an object's observable state never changes. Both reduce temporal coupling and simplify safe sharing.
Copy on input and output¶
public final class Route {
private final List<String> stops;
public Route(Collection<String> stops) {
this.stops = List.copyOf(stops);
if (this.stops.stream().anyMatch(Objects::isNull)) {
throw new IllegalArgumentException("stops cannot contain null");
}
}
public List<String> stops() {
return stops;
}
}
List.copyOf prevents structural modification through the returned list and
disconnects it from a mutable source collection. It is a shallow copy: if an
element is mutable, callers can still mutate reachable state. Deep immutability
requires immutable elements or explicit copies at every ownership boundary.
Arrays, Date, mutable builders, buffers, and collections often need copies on
input and output. An unmodifiable view is not a copy; changes through another
reference remain visible.
Records and builders¶
Java records make data-carrier syntax concise but do not make referenced components deeply immutable. Use a compact constructor to normalize and copy mutable components. A builder may remain mutable while constructing a final immutable value, but the built object must not retain mutable builder storage.
Concurrency and publication¶
Properly constructed immutable objects are easier to publish and share because readers do not coordinate later mutations. This does not make operations on external mutable resources atomic. A snapshot also becomes stale; freshness is a separate contract.
Trade-offs¶
Copies cost time and memory. Prefer clear ownership transfer, immutable value types, persistent structures, or bounded snapshots where these express the contract. Measure before avoiding a necessary copy, and document any API that borrows storage whose lifetime or mutation remains controlled by the caller.
Test mutation of original inputs, returned values, and nested elements. Also verify equality and hash stability when values are used in hash-based collections or cache keys.