The full picture
class CountingSet<E> extends HashSet<E> {
private int adds = 0;
@Override public boolean add(E e) { adds++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
adds += c.size(); return super.addAll(c); // HashSet.addAll calls add()…
} // -> every element counted TWICE
}
// Composition (forwarding) is immune to the superclass's self-use:
class CountingSet<E> implements Set<E> {
private final Set<E> inner; // delegate
private int adds = 0;
public boolean add(E e) { adds++; return inner.add(e); }
public boolean addAll(Collection<? extends E> c) {
int before = adds; boolean r = false;
for (E e : c) r |= add(e); // OUR contract, our counting
return r;
}
// …forward the rest
}- Ask 'is-a and substitutable-under-contract?' — if a
Square extends Rectanglebreaks setWidth expectations, it's not LSP-is-a. - Inheritance across module/team boundaries is a versioning trap: superclass evolves, subclasses in other repos break silently. Design for it (
abstracthooks, documented self-use) or prohibit it (final, sealed). - Modern Java nudges composition: records can't extend classes; sealed interfaces + pattern matching model closed variants without deep trees; default methods let interfaces carry shared behavior.