Refused bequest anti-pattern
Learn why subclasses that ignore or override inherited methods signal a broken IS-A relationship, and how interface segregation resolves the LSP violation cleanly.
TL;DR
- A refused bequest happens when a subclass inherits methods it cannot honor, throwing exceptions, returning empty values, or silently doing nothing.
- Every
UnsupportedOperationExceptionon an inherited method is an LSP violation screaming at you. - The root cause is inheritance for code reuse instead of inheritance for subtyping.
- Interface Segregation Principle (ISP) fixes this: split fat interfaces into capability-based contracts so classes only implement what they genuinely support.
- Java's own standard library has this bug baked in:
Stack extends Vector,Properties extends Hashtable.
The Problem
Your team builds a collection library. Someone creates ImmutableList by extending MutableList because "it already has get() and size() implemented." The subclass overrides every mutation method to throw an exception. Code that accepts a MutableList parameter now explodes at runtime when it receives an ImmutableList.
// MutableList provides read and write operations
public class MutableList<T> {
protected List<T> items = new ArrayList<>();
public T get(int index) { return items.get(index); }
public int size() { return items.size(); }
public void add(T item) { items.add(item); }
public void remove(int index) { items.remove(index); }
public void clear() { items.clear(); }
}
// ImmutableList refuses 3 out of 5 inherited methods
public class ImmutableList<T> extends MutableList<T> {
public ImmutableList(List<T> source) {
this.items = new ArrayList<>(source);
}
@Override
public void add(T item) {
throw new UnsupportedOperationException("List is immutable");
}
@Override
public void remove(int index) {
throw new UnsupportedOperationException("List is immutable");
}
@Override
public void clear() {
throw new UnsupportedOperationException("List is immutable");
}
}
I've debugged production outages caused by exactly this pattern. A service passes an ImmutableList to a utility that calls add(), and the application crashes at 3 AM with an UnsupportedOperationException buried three stack frames deep.
The Liskov Substitution Principle says: if S is a subtype of T, then objects of type T can be replaced with objects of type S without breaking the program. ImmutableList violates this completely. It is not a MutableList. The inheritance relationship is a lie.
Why It Happens
- Inheritance for code reuse. The developer sees "ImmutableList needs
get()andsize(), and MutableList already has them," so they extend. The motivation is DRY, not subtyping. - Top-down hierarchy design. The base class is designed first with every conceivable method, then subclasses cherry-pick what they support. The hierarchy should be designed from the leaf up, based on what each type genuinely needs.
- The Square/Rectangle trap. Developers model real-world IS-A relationships ("a square IS a rectangle") without checking behavioral substitutability. A square that refuses
setWidth()independent ofsetHeight()breaks any code expecting a rectangle. - Framework inherited baggage. Extending a framework class to reuse infrastructure pulls in methods the subclass cannot support. The subclass stubs them out "temporarily," and the stubs become permanent.
Java's own refused bequests
java.util.Stack extends Vector, inheriting get(int), add(int, E), and insertElementAt(). A stack should not allow random index access. java.util.Properties extends Hashtable, inheriting put(Object, Object), but Properties only supports String keys and values. Both are refused bequests baked into the JDK since Java 1.0 and cannot be fixed without breaking backwards compatibility.
How to Detect It
| Signal | What It Means | How to Check |
|---|---|---|
Methods throw UnsupportedOperationException | Subclass refuses the inherited contract | Search codebase for throw new UnsupportedOperationException |
| Methods override to do nothing (empty body) | Silent refusal, callers assume the operation succeeded | Look for overrides with empty bodies or no-op returns |
Runtime instanceof checks before calling methods | Callers compensate for the broken substitutability | Grep for instanceof checks on hierarchy members |
| Subclass overrides 50%+ of parent methods | The IS-A relationship is likely false | Count overridden methods vs. inherited methods |
| Unit tests skip mutation methods on "read-only" subclasses | Test authors know substitution is unsafe | Check if subclass tests avoid exercising parent behavior |
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.