Card 06/ 09
Liskov in the Wild: Java's Unmodifiable List
It is a List, and add() throws UnsupportedOperationException.
Java's standard library contains a deliberate Liskov trade-off, and you meet it every time you call List.of. This example shows it and what it costs callers. It shares one thing with the square: the subtype's method exists, and its behaviour breaks what the caller assumed.
List.of(...) returns an unmodifiable list. It implements java.util.List, so it can be passed anywhere a List is expected. But List declares add, and an unmodifiable list cannot add. The method below adds a default tag to whatever list it is given.
import java.util.ArrayList;
import java.util.List;
public class LspUnmodifiable {
static void addDefault(List<String> tags) {
tags.add("new");
System.out.println("added: " + tags);
}
public static void main(String[] args) {
addDefault(new ArrayList<>(List.of("sale")));
try {
addDefault(List.of("sale"));
} catch (UnsupportedOperationException error) {
System.out.println("failed: " + error.getClass().getName());
}
}
}added: [sale, new]
failed: java.lang.UnsupportedOperationExceptionThe ArrayList accepts the new tag. The list from List.of throws UnsupportedOperationException. Nothing in the type of the tags parameter warned addDefault that this could happen, and the compiler accepted both calls.
The JDK designers knew this. The documentation of List.add marks it as an "optional operation", which writes the possibility of refusal into the contract itself. So, strictly, the unmodifiable list keeps the letter of List's contract: the contract already said add might throw. The cost is that the promise of List is weaker than it looks, and every caller that wants to add must either trust where its list came from or copy it first, as new ArrayList<>(...) does in the listing.
The alternative was a separate ReadOnlyList interface without add, which other languages chose. Kotlin, which runs on the same virtual machine, has a read-only List and a separate MutableList. Java chose one broad interface to keep the collections library small, and accepted a contract with a hole in it. Knowing that the hole is there is what lets you write callers that do not fall into it.
← → move through sections and cards