Refused Bequest — Specification¶
This document defines refused bequest precisely enough that two engineers can disagree about a class and resolve it with measurement, not opinion.
1. Informal definition¶
Refused bequest occurs when a subclass inherits methods, fields, or invariants from its superclass that it does not want, does not use, or actively contradicts. The subclass "refuses" the inheritance — through empty overrides, UnsupportedOperationException, no-op stubs, or hidden narrowing of the contract.
The word "bequest" is deliberate: inheritance in OO is treated as a legal will, where the parent class leaves its protected and public surface area to its children. A refused bequest is a child class that has been left an asset (the inherited API) that it does not want and cannot give back.
2. Formal definition¶
Let C be a class with superclass P. Let:
M(P)= the set of non-private, non-final methods declared inP(or transitively inherited byP).O(C)= the set of methods ofM(P)thatCoverrides.R(C)= the subset ofO(C)whose override body is one of:- empty (no statements),
- a single
throw new UnsupportedOperationException(...), - a single
throw new AssertionError(...), - a single
returnwith a default value and no use ofthisstate, - a body that calls
super.foo(...)and does nothing else. U(C)= the subset ofM(P)thatCuses — i.e., references viasuper.foo()or relies on as inherited behavior without overriding.
Then we define two metrics:
NOM — Number of Overridden Methods¶
NOM by itself is not a smell. A class that overrides 8 of 10 inherited methods is doing customization, not refusal.
NORM — Number Of Refused Methods¶
Refusal ratio¶
A class refuses its bequest when:
or, additionally, when |R(C)| >= 1 and the refused method is part of the supertype's documented contract (i.e., it appears in the Javadoc of P as a method callers may rely on).
The second condition matters because a single refused method is enough to break Liskov substitution if it's a method polymorphic callers actually use.
3. Thresholds¶
Empirical thresholds from industrial codebases (drawn from PMD defaults, SonarSource rule configurations, and Lanza & Marinescu's Object-Oriented Metrics in Practice):
| Metric | Healthy | Warning | Smell |
|---|---|---|---|
| NOM(C) | ≤ 4 | 5–9 | ≥ 10 |
| NORM(C) | 0 | 1 | ≥ 2 |
| refusal_ratio(C) | 0 | 0.05–0.25 | > 0.25 |
| inherited_usage | ≥ 0.5 | 0.25–0.5 | < 0.25 |
inherited_usage(C) = |U(C)| / |M(P)| measures how much of the inheritance the subclass actually leverages. Below 25% means the subclass barely uses what it inherits — it should probably compose, not extend.
4. Refusal categories¶
Not all refusals are equal. There are four distinct kinds, each with a different fix:
Category A — Explicit contract refusal¶
Fix: Extract a narrower interface. The subclass is telling you the bequest is wrong.
Category B — Silent narrowing¶
@Override public void setBalance(Money m) {
if (m.isNegative()) return; // silently refuses negative input
this.balance = m;
}
Fix: This is worse than Category A because the refusal is invisible to callers. Throw, or — better — strengthen the parent's contract to disallow the input.
Category C — Type narrowing refusal¶
public class Properties extends Hashtable<Object, Object> {
// documented to require String keys and values,
// but inherits put(Object, Object) which it cannot remove
}
Fix: Composition. The parent's type contract is too wide and inheritance cannot remove members.
Category D — Interface refusal¶
class ReadOnlyView implements List<E> {
public boolean add(E e) { throw new UnsupportedOperationException(); }
// ... refuses every mutator
}
Fix: Implement a narrower interface (Collection? Iterable?) instead of List. If no narrower interface exists, this is a signal that the standard library is missing one — consider creating your own.
5. What is not refused bequest¶
Be precise about the boundary. The following are not refused bequests:
- Template Method overrides — empty default implementations in the parent are intentional extension points. Subclasses opting in by overriding and opting out by inheriting the no-op are using the pattern correctly.
- Adapter classes with empty event-handler methods (e.g.,
MouseAdapter) — the parent class exists specifically so callers can override only the methods they care about. - Strengthening preconditions in a way that's documented as a subtype contract — though this is an LSP violation by another name.
- Optional operations explicitly documented in the supertype —
Collection.addis documented to optionally throwUnsupportedOperationException. Subclasses doing so are honoring, not refusing, the bequest.
The distinction in case 4 is subtle but important: the JDK Collections framework chose to make mutation methods optional, which builds refused bequest into the supertype's contract. This is widely considered a design mistake (Bloch, Effective Java 3e, Item 18, "Favor composition over inheritance") but it is technically not refusal — the supertype gave the subtype permission.
6. Detection rules in static analyzers¶
SonarJava¶
| Rule ID | Title | What it catches |
|---|---|---|
S1185 | Overriding methods should do more than simply call the same method in the super class | @Override foo() { super.foo(); } — pure refusal disguised as override |
S1186 | Methods should not be empty | Catches empty override bodies |
S2638 | Method overrides should not change contracts | Subclass throws an exception the parent doesn't declare |
S1190 | Reserved keywords should not be used as identifiers | (Tangentially related — narrowing) |
S125 | Sections of code should not be commented out | Detects refusal by deletion |
PMD¶
| Rule | Category | What it catches |
|---|---|---|
EmptyMethodInAbstractClassShouldBeAbstract | bestpractices | Empty body in abstract class → likely template for refusal |
UncommentedEmptyMethodBody | documentation | Empty methods without explanation |
AvoidThrowingNullPointerException | design | Often paired with UOE in refusals |
OverrideBothEqualsAndHashcode | bestpractices | Partial refusal of Object contract |
SignatureDeclareThrowsException | design | Refusing the parent's narrower exception contract |
Checkstyle¶
| Check | What it catches |
|---|---|
MissingOverride | Catches accidental overloads that look like refusals |
EmptyBlock | Empty method bodies |
IllegalThrows | Configurable to ban UnsupportedOperationException in production |
Custom ArchUnit rule for NORM¶
@ArchTest
static final ArchRule norm_under_threshold = classes()
.that().areNotInterfaces()
.and().areNotAnnotatedWith(Deprecated.class)
.should(haveNormAtMost(1));
The implementation of haveNormAtMost(int) walks the bytecode, counts overrides whose body matches one of the refusal shapes (empty, single-throw, single-super-call), and compares to the threshold.
7. Relation to other metrics¶
- LCOM (Lack of Cohesion of Methods) — high LCOM in a subclass often correlates with refused bequest, because the subclass has two clusters of methods: ones using inherited state and ones refusing it.
- DIT (Depth of Inheritance Tree) — refused bequest tends to appear deeper in the tree (DIT ≥ 3). Each level adds bequests the next level may refuse.
- NOC (Number of Children) — a parent with many children is more likely to have at least one child that refuses, simply by combinatorics.
- WMC (Weighted Methods per Class) — inflated by inherited methods the subclass refuses, since the subclass-as-seen-by-callers exposes them anyway.
8. Decision flowchart¶
Does C override a method of P?
|
yes / no
/ \
Look at the body Not relevant — done.
|
+-----------+-----------+
| | |
empty throws UOE delegates only to super
| | |
+-----------+-----------+
|
v
REFUSED — increment NORM(C)
|
Is the refused method in P's documented contract?
|
yes / \ no
/ \
LSP violation Lesser concern; still smell
— must fix — fix if NORM ≥ 2 or
refusal_ratio > 0.25
9. Worked example — measuring NORM¶
abstract class Animal {
public void breathe() { /* default */ }
public void eat() { /* default */ }
public void sleep() { /* default */ }
public void reproduce(){ /* default */ }
public abstract void move();
public void makeSound(){ /* default */ }
}
class Fish extends Animal {
@Override public void move() { swim(); }
@Override public void makeSound() {
throw new UnsupportedOperationException("Fish don't vocalize");
}
private void swim() { ... }
}
M(Animal) = { breathe, eat, sleep, reproduce, move, makeSound }, so|M(P)| = 6.O(Fish) = { move, makeSound }, soNOM(Fish) = 2.R(Fish) = { makeSound }(single-throw refusal), soNORM(Fish) = 1.refusal_ratio = 1/6 ≈ 0.17— in the warning band.U(Fish)includes the inheritedbreathe,eat,sleep,reproduce—|U| = 4, soinherited_usage = 4/6 ≈ 0.67. Healthy.
Verdict: Fish is borderline. The single refusal is real (LSP issue if any caller does for (Animal a : zoo) a.makeSound();), but the inheritance is otherwise pulling its weight. Fix: split Vocalizing into its own interface.
10. Canonical sources¶
The definitions above are operationalizations of claims made in the primary literature. Map each back to its source before citing it in a design review.
Where the smell is named and the fixes are specified¶
- Fowler, Refactoring: Improving the Design of Existing Code, 2nd ed. (2018) is the authoritative catalog. Refused Bequest appears in the "Smells" chapter (Ch. 3): "Subclasses get to inherit the methods and data of their parents. But what if they don't want or need what they are given?" Fowler's prescribed refactorings, all defined with mechanics in the same book:
- Replace Subclass with Delegate — convert
extends Pinto a field of typeP(or an interface) and forward only the methods you want. This is the canonical "Replace Inheritance with Delegation" move. - Replace Superclass with Delegate — the symmetric move when the parent, not the child, is the wrong abstraction (the
Stack extends Vectorcase). - Push Down Method / Push Down Field — when a member lives in the parent but only one subclass uses it, move it down so siblings stop inheriting (and refusing) it.
- Extract Superclass / Extract Interface — when refusal signals that two classes share some behavior, hoist only the shared, wanted part into a new common supertype or a narrower interface.
- Fowler explicitly notes that a small refused bequest (a subclass that doesn't want a few inherited members) is often tolerable; the smell becomes actionable when the subclass refuses the behavior and the interface, i.e., it doesn't even want to be substitutable.
Why refusal of a documented method is an LSP break¶
- Liskov & Wing, "A Behavioral Notion of Subtyping," ACM TOPLAS 16(6), Nov. 1994 is the formal basis. The subtype requirement (the "subtype methods" rule plus history/invariant constraints) is: for subtype
SofT, every property provable about objects ofTmust hold for objects ofS. An override that throwsUnsupportedOperationExceptionwhere the supertype documents the method as usable strengthens the precondition (it now requires "don't call me"), violating the methods rule. This is the precise sense in which §2's "refused method in the documented contract" condition is an LSP violation, not just a stylistic complaint. (The 1994 TOPLAS paper supersedes Liskov's earlier 1988 OOPSLA keynote where the "substitution property" was first stated informally.)
Why the fix is composition, not a deeper hierarchy¶
- Bloch, Effective Java, 3rd ed. (2018):
- Item 18 — "Favor composition over inheritance." Bloch's argument is exactly the refused-bequest cure: inheritance violates encapsulation because the subclass depends on implementation details of the superclass; wrapping (a forwarding class plus a reusable forwarding interface — the decorator-style "InstrumentedSet" example) lets you expose only the operations you want. This is the standard Java idiom for Replace Subclass with Delegate.
- Item 19 — "Design and document for inheritance or else prohibit it." The complement: if a class is not designed and documented as a base class, it should be
final(or have package-private constructors) so no one can refuse its bequest in the first place. - Meyer, Object-Oriented Software Construction, 2nd ed. (1997) frames the same constraint through Design by Contract: a redefinition may weaken a precondition and strengthen a postcondition, never the reverse. Throwing on an inherited operation strengthens the precondition and is therefore an illegal redefinition under DbC — the contract-level statement of refused bequest.
- Gamma, Helm, Johnson, Vlissides (GoF), Design Patterns (1994) — the chapter-one maxim "Favor object composition over class inheritance" and the Decorator and Adapter patterns are the structural realizations of the delegation fix.
Numbered reading list¶
- Martin Fowler, Refactoring, 2nd ed. (2018) — Ch. 3 "Refused Bequest"; refactorings "Replace Subclass with Delegate," "Replace Superclass with Delegate," "Push Down Method/Field," "Extract Superclass," "Extract Interface."
- Barbara Liskov & Jeannette Wing, "A Behavioral Notion of Subtyping," ACM TOPLAS 16(6), 1994 — the subtype methods/invariant/history rules.
- Joshua Bloch, Effective Java, 3rd ed. (2018) — Item 18 (favor composition over inheritance), Item 19 (design for inheritance or prohibit it), Item 20 (prefer interfaces to abstract classes).
- Bertrand Meyer, Object-Oriented Software Construction, 2nd ed. (1997) — Design by Contract and the rules for valid redefinition.
- Gamma et al., Design Patterns (1994) — Decorator, Adapter, and "favor composition over inheritance."
- Lanza & Marinescu, Object-Oriented Metrics in Practice (2006) — the NOM/DIT/WMC metric thresholds referenced in §3.
Memorize this¶
Refused bequest is measurable, not a matter of taste. Compute NORM and the refusal ratio; cross-check against PMD, SonarJava, and ArchUnit. If
refusal_ratio > 0.25or any refused method is in the supertype's documented contract, the smell is real and the fix is to extract a narrower interface or switch to composition.
In this topic