Medium zalando java

Why prefer immutable objects, and how is an unmodifiable view different from an immutable copy?

The 30 second answer

Immutable objects can be shared between threads without locks once they are safely published, and they make reasoning and caching easier. An unmodifiable view, like Collections.unmodifiableList, only blocks writes through that reference; if someone changes the backing list, the view changes too. An immutable copy, like List.copyOf, owns its data. To make a class immutable, copy mutable inputs and never expose them.

Why immutability. State that never changes cannot be seen half-updated, so after safe publication, for example through final fields, many threads can read it without synchronization. Immutable objects are safe map keys and cache values, and their invariants are checked once in the constructor. Zalando's 2016 Java preparation post asks candidates to "embrace immutability", understand "the difference between unmodifiable views and immutable copies", and know how to make a class immutable when it holds mutable state.

View versus copy.

  • Collections.unmodifiableList(list) returns, in the Javadoc's words, an unmodifiable view. Writes through the view throw, but changes to the underlying list show through.
  • List.copyOf(list) and List.of(...) return unmodifiable lists that do not share the caller's backing array. They reject nulls. Their elements can still be mutable objects, so the list is only shallowly immutable.
List<String> source = new ArrayList<>(List.of("a"));
List<String> view = Collections.unmodifiableList(source);
List<String> copy = List.copyOf(source);
source.add("b");
// view is now [a, b]; copy is still [a]

Making a class immutable. Make it final, or give it private constructors. Make every field private and final. Copy mutable inputs in the constructor, such as arrays, lists and Date, and return copies or unmodifiable views from getters. Do not let this escape during construction. A record gives final fields and generated accessors, but you still copy mutable components in a compact constructor.

record Basket(String id, List<String> skus) {
    Basket { skus = List.copyOf(skus); }   // defensive copy; the record stays immutable
}

Alternate interview scenarios

"Why store a password in a char[] rather than a String?" The Java Cryptography Architecture guide explains it: "Objects of type String are immutable, i.e., there are no methods defined that allow you to change (overwrite) or zero out the contents of a String after usage", which makes them unsuitable for passwords. A char[] can be overwritten with Arrays.fill(password, '\0') as soon as it has been used, which shortens the time the secret sits in memory and can show up in a heap dump. String literals are also interned in the string pool, so equal literals share one object; see the string pool scenario. It is a mitigation, not a guarantee: the garbage collector may already have copied the array, and frameworks often turn it into a String anyway.

Sources (5)
  1. Collections Javadoc (Java 21): unmodifiable views
  2. List Javadoc (Java 21): unmodifiable lists from List.of and List.copyOf
  3. Zalando Engineering: Interview preparation tips for Java developers (June 2016)
  4. JLS 21 chapter 17: Threads and Locks (memory model, happens-before, final fields)
  5. Java Cryptography Architecture Reference Guide (Java 21): why passwords are collected in char arrays