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.
A refused bequest is an inheritance smell: a subtype receives a parent contract but cannot honor part of it. The result is a type that looks substitutable in signatures while throwing, returning a sentinel, or silently doing nothing at runtime.
TL;DR
- A refused bequest happens when a subclass inherits methods it cannot honor, throwing exceptions, returning empty values, or silently doing nothing.
- An
UnsupportedOperationExceptionin an inherited method is a strong signal that the subtype cannot honor the parent contract; confirm it with a substitution test. - 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 standard library contains historical examples of awkward inheritance, including
Stack extends VectorandProperties extends Hashtable.
The Problem
A subtype should be usable wherever its parent type is expected, without surprising callers. Symptoms include overrides that throw UnsupportedOperationException, return dummy values, or ignore the call, plus callers that add instanceof checks to avoid those behaviors. This matters because the type system advertises capabilities that the object does not actually provide.
Concrete Example
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");
}
}
The failure can be far from the construction site: a service passes an ImmutableList to a utility that calls add(), and the application crashes with an UnsupportedOperationException several 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
Diagnose the smell by checking whether a subtype can honor every behavior promised by its parent, not merely whether it shares fields or method signatures.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.