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, Item 19) 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.
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.