Refused Bequest — Junior¶
What? Refused Bequest is the code smell that appears when a subclass inherits methods and fields from its parent but refuses most of them — by overriding them to throw
UnsupportedOperationException, by leaving the body empty, or by quietly doing nothing. The subclass takes the inheritance "bequest" the parent leaves it, then declines to use it. The classic example is anImmutableListthat extendsArrayListand throws on every mutating method. How? When you seethrow new UnsupportedOperationException()inside an override, or an@Overridemethod whose body is{ /* no-op */ }, you are almost certainly looking at refused bequest. The fix is rarely "fill in the body". The fix is usually to stop inheriting — replaceextendswith composition, or split the parent into smaller interfaces the subclass actually wants.
1. The smell in one paragraph¶
Inheritance gives a child class everything the parent has: fields, methods, contracts, exceptions. That's the bequest. Refused bequest happens when the child accepts the syntactic form of inheritance (it compiles, it passes instanceof, it can be assigned to a parent variable) but rejects the behavioural form — it doesn't want add to actually add, doesn't want setEnabled(false) to actually disable, doesn't want start() to actually start. The result is a type that lies: callers think they have a List, but list.add(x) blows up.
This is one of the most important smells in Java because the JDK itself ships textbook examples — Stack extends Vector, Properties extends Hashtable<Object,Object>, Collections.unmodifiableList — and they cause real bugs in production code that copies the pattern.
2. The canonical example: ImmutableList extends ArrayList¶
public class ImmutableList<T> extends ArrayList<T> {
public ImmutableList(Collection<? extends T> source) {
super(source);
}
@Override public boolean add(T element) {
throw new UnsupportedOperationException("immutable");
}
@Override public T remove(int index) {
throw new UnsupportedOperationException("immutable");
}
@Override public T set(int index, T element) {
throw new UnsupportedOperationException("immutable");
}
@Override public void clear() {
throw new UnsupportedOperationException("immutable");
}
// ... and 8 more mutating methods, all refusing the bequest.
}
What's wrong:
- It lies about its type.
ImmutableList<T>is-aArrayList<T>is-aList<T>. TheListcontract permitsadd. A method that takesList<T>and callsaddis correct — and crashes when you hand it anImmutableList. - It violates Liskov substitution. You cannot substitute
ImmutableListforArrayListwithout breaking callers. The whole point of subtyping is broken. - The failure mode is runtime, not compile-time. The compiler is happy. The user sees
UnsupportedOperationExceptionin production logs. - It carries dead inheritance baggage.
ImmutableListinheritselementData,modCount,size— internalArrayListmachinery it doesn't need. Memory wasted, behaviour ambiguous (what doesmodCountmean for an immutable list?).
3. What the JDK itself does — and why it's still a smell¶
Look at java.util.Collections#unmodifiableList:
// Inside java.util.Collections (simplified):
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
implements List<E> {
public boolean add(E e) { throw new UnsupportedOperationException(); }
public E remove(int index) { throw new UnsupportedOperationException(); }
public E set(int index, E element) { throw new UnsupportedOperationException(); }
// ...
}
The JDK chose this design in Java 1.2 (1998) and we're stuck with it. Modern Java code that needs immutability uses List.of(...) — which still throws UnsupportedOperationException on mutation, because there is no ImmutableList interface separate from List in the JDK. The smell is baked into the standard library.
The lesson is not "the JDK does it, so it's fine". The lesson is: the JDK got this wrong, and the cost of fixing it would break too much existing code. In your code, where you don't have 28 years of backwards-compatibility debt, do better.
4. Two more textbook cases¶
4.1 Stack extends Vector¶
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.push(2);
stack.push(3);
stack.add(0, 99); // legal — Vector.add(index, element) is inherited
System.out.println(stack.pop()); // 3 — but the stack is now corrupted
Stack is meant to be LIFO. But because it inherits from Vector, callers can call add(int, E), remove(int), insertElementAt, and reach into the middle of the stack. The bequest Stack should refuse is most of Vector's API. It doesn't refuse it — it just hopes you won't notice. That's even worse than throwing.
4.2 Properties extends Hashtable<Object, Object>¶
Properties props = new Properties();
props.setProperty("host", "localhost");
props.put(42, new Date()); // legal — Hashtable.put accepts any Object
String h = props.getProperty("host"); // "localhost"
String d = props.getProperty("42"); // null — keys must be Strings to be visible
Properties is meant to be Map<String, String>. But it extends Hashtable<Object, Object>, so callers can put any object as key or value. Properties accepts the bequest only by convention, not by type. The JDK now formally discourages Hashtable methods on Properties but cannot remove them.
5. The fix: composition over inheritance¶
For ImmutableList, don't extend ArrayList. Wrap it.
public final class ImmutableList<T> implements Iterable<T> {
private final List<T> backing;
public ImmutableList(Collection<? extends T> source) {
this.backing = List.copyOf(source); // defensive copy + already immutable
}
public int size() { return backing.size(); }
public T get(int i) { return backing.get(i); }
public boolean contains(Object o) { return backing.contains(o); }
@Override public Iterator<T> iterator() {
return Collections.unmodifiableList(backing).iterator();
}
public Stream<T> stream() { return backing.stream(); }
}
Now:
- There is no
addmethod to refuse. The class doesn't lie. - A caller who has an
ImmutableList<T>cannot try to mutate it — the compiler stops them. - The Liskov question doesn't arise —
ImmutableListisn't a subtype ofList. - You picked exactly the operations you want to expose.
The trade-off: you lose the ability to pass ImmutableList to a method that takes List<T>. If that matters, expose a read-only view via List.copyOf(backing) from a method like asList().
6. How to recognise it in code review¶
The strongest signals, ordered by severity:
throw new UnsupportedOperationException(...)inside@Override— almost always refused bequest. Search for it in your codebase.- Empty overrides —
@Override void onResume() {}in a framework subclass. The subclass says it handlesonResumebut actually doesn't. This is a softer refused bequest. - Overrides whose body is
// intentionally left blank— same pattern, just commented. @Deprecatedoverrides — the parent has a method, the subclass overrides it to do nothing and marks it deprecated so callers stop using it. The deprecation is an admission the bequest was unwanted.- Class-level Javadoc that says "do not call methods X, Y, Z" — if the documentation has to warn callers off inherited methods, those inherited methods are refused.
7. Why it's worse than other smells¶
Most code smells (long method, large class, duplicate code) make code harder to read. Refused bequest is different — it makes code lie about its type. The type system is supposed to be your safety net. A class that throws on inherited methods turns the safety net into a tripwire.
The consequences:
- Static analysis is fooled. Tools that check "does this code call
List.add?" say yes. They cannot tell that this specificListwill throw. - Polymorphism is poisoned. Any code that takes a
Listparameter must now defensively avoid the throwing methods, or wrap calls in try/catch. - Refactoring is dangerous. "Extract this method to take a
Collectioninstead of aList" might suddenly work in tests and break in production because production passed an immutable list.
8. A first-day rule¶
If you're about to write extends SomeConcreteClass, ask:
Will I use every method I'm inheriting?
If the answer is "no, I'll override a few to throw" — stop. You don't want inheritance. You want composition, or you want a narrower interface. Inheritance is for is-a. If you find yourself refusing the parent's bequest, you don't have an is-a relationship. You have a has-a or a behaves-like-some-of relationship, and Java has cleaner tools for both.
9. Quick checklist¶
- No
throw new UnsupportedOperationException()in@Overridemethods in production code. - No empty
@Overridebodies that aren't documented as deliberate hooks. - If you
extendsa concrete class, you use all of its public methods meaningfully. - Immutable variants do not extend mutable parents.
- When in doubt, prefer composition + a narrow interface over inheritance.
10. What's next¶
| Topic | File |
|---|---|
| Why refused bequest happens; refactoring playbook | middle.md |
| LSP lens, Pull/Push Down Method, JDK trade-offs | senior.md |
| Detection (ArchUnit), legacy migration | professional.md |
| Formal definition, NOM thresholds, PMD/SonarJava rules | specification.md |
| 10 numbered diagnosis scenarios | find-bug.md |
| JIT inlining, vtable cost, deopt risk | optimize.md |
| 8 practice exercises with validation | tasks.md |
| 20 Q&A interview prep | interview.md |
Related smells and principles:
| Topic | Path |
|---|---|
| Fragile Base Class Problem | ../../03-design-principles/05-fragile-base-class/ |
| Liskov Substitution Principle | ../../03-design-principles/01-solid-principles/ |
| Composition over Inheritance | ../../03-design-principles/02-composition-over-inheritance/ |
Memorize this: Refused bequest is a subclass that throws or no-ops on methods it inherited. It lies about its type, breaks Liskov substitution, and the type system stops protecting callers. The fix is rarely "fill the body" — the fix is to stop inheriting. Wrap instead of extend; pick the operations you want; let the compiler enforce the rest.