Why a Contract‑Compliant List Still Breaks Liskov Substitution in Java
The article demonstrates that although List.of, Arrays.asList, and unmodifiableList implement the List interface, they violate the Liskov Substitution Principle because their optional add/remove operations throw UnsupportedOperationException, revealing a hidden contract mismatch between interface documentation and runtime behavior.
What the Problem Looks Like
In Java a puzzling situation arises: two objects are both declared as List, yet one allows add while the other throws an exception.
List<String> list = List.of("A", "B");
list.add("C");The code compiles, but at runtime it throws UnsupportedOperationException. Replacing List.of() with new ArrayList<>() makes the same code run without error.
Liskov Substitution Principle (LSP)
LSP states that any place a base class is used should be replaceable with a subclass without changing program behavior. In Java terms, code that depends only on the parent type should produce the same result when the concrete instance is swapped for any subclass.
The key is that the decision is based on runtime behavior—method calls, return values, and exceptions—not merely on the declared type.
Understanding the Contract
Before writing code we must ask: what does code that depends only on the parent type actually rely on? The answer is the contract promised by the interface. For java.util.List the Javadoc promises that after add the size increases, get(0) returns the first element added, remove decreases the size, and iteration order matches insertion order.
There are two layers of contract: the explicit Javadoc promises and the implicit expectations programmers have (e.g., that a List is mutable). When these layers diverge, problems appear.
Verification Code
public static void main(String[] args) {
verify("ArrayList", new ArrayList<>());
verify("LinkedList", new LinkedList<>());
verify("Arrays.asList", Arrays.asList());
verify("List.of", List.of());
verify("Collections.unmodifiableList", Collections.unmodifiableList(new ArrayList<>()));
}
static void verify(String name, List<String> list) {
try {
// Contract 1: size after two adds should be 2
list.add("a");
list.add("b");
if (list.size() != 2) {
System.out.println(name + " : FAIL, size after add is " + list.size());
return;
}
// Contract 2: get(0) returns first added element
if (!"a".equals(list.get(0))) {
System.out.println(name + " : FAIL, get(0) not first element");
return;
}
// Contract 3: iteration order matches insertion order
String firstInLoop = null;
for (String s : list) { firstInLoop = s; break; }
if (!"a".equals(firstInLoop)) {
System.out.println(name + " : FAIL, first iterated element wrong");
return;
}
// Contract 4: size after remove should be 1
list.remove(0);
if (list.size() != 1) {
System.out.println(name + " : FAIL, size after remove is " + list.size());
return;
}
System.out.println(name + " : PASS, all four contracts satisfied");
} catch (UnsupportedOperationException e) {
System.out.println(name + " : FAIL, threw " + e);
}
}The verification function only uses methods declared in the List interface. If any contract fails, the test reports a failure.
Results on Windows 11 (Java 17)
ArrayList : PASS, all four contracts satisfied
LinkedList : PASS, all four contracts satisfied
Arrays.asList : FAIL, threw java.lang.UnsupportedOperationException
List.of : FAIL, threw java.lang.UnsupportedOperationException
Collections.unmodifiableList : FAIL, threw java.lang.UnsupportedOperationExceptionOnly ArrayList and LinkedList pass; the other three fail at the first contract (the add operation).
Why the Mismatch Exists
The JDK’s Collection Javadoc marks certain mutating operations as *optional*. Implementations may choose not to support them and throw UnsupportedOperationException when invoked.
Examples: Arrays.asList returns a fixed‑size view of an array; you can modify existing elements but cannot change the size. List.of creates an immutable list. Collections.unmodifiableList wraps another list with a read‑only façade.
From a pure Javadoc standpoint these classes do not violate the interface, because the optional nature of add and remove is documented. However, callers typically expect a List to be mutable, a second‑layer expectation not captured in the interface contract.
Other LSP Pitfalls
1. Subclass adds extra restrictions. HashSet accepts any object, while TreeSet requires elements to be comparable. Inserting a non‑comparable object into a TreeSet throws ClassCastException, whereas the same code works with HashSet.
2. Subclass does not support a parent operation. The three list implementations above illustrate this case.
3. Subclass breaks its own invariants. Properties inherits Hashtable.put(Object, Object), which accepts any objects, but Properties expects both keys and values to be String. Non‑string entries are accepted at put time and cause ClassCastException later when store is called.
Design Takeaways
When designing an interface, ask two questions:
Does every implementation fulfill each promise in the interface documentation?
If an implementation cannot fulfill a promise, does the interface provide a way for callers to detect that limitation beforehand?
If the answer to both is yes, the inheritance hierarchy is healthy. Otherwise, hidden violations of LSP may surface at runtime, surprising callers who trusted the interface signature.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
samdeepthink
Knowledge Planet: Old Dock's Tech Chronicles Zhihu: SamDeepThinking A technical manager who still codes heavily on the front line. From junior developer to tech lead, then tech manager, now leading the whole front‑ and back‑end development team—leveling up along the way. I have some insights on programming, career development, and tech management.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
