Skip to content

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.