Modern Java in Production
ID
Lesson 4 of 9 · 14m

Objects, inheritance, and how failure travels

When an abstract class earns its place, why composition usually wins, what static really means, and the difference between an exception a caller can act on and one it cannot.

Interface or abstract class

An interface declares a capability; an abstract class shares an implementation. Reach for the abstract class only when there is real state or a real algorithm to share — a template method with hooks, a base that holds fields every subclass needs. Everything else is an interface, because a class can implement many and extend exactly one, and that one slot is worth keeping free.

Default methods blurred this, and the rule of thumb still holds: if you are writing an abstract class with no fields and no constructor, you wrote an interface.

Composition usually wins

Inheritance couples you to a superclass’s implementation, not just its contract. Change the parent and every subclass is at risk — the fragile base class problem, and the reason ArrayList is not a good thing to extend.

class AuditedRepo implements Repo { // composition
private final Repo delegate;
public void save(Order o) { audit(o); delegate.save(o); }
}

That class can wrap any Repo, be tested with a fake, and cannot be broken by a change inside delegate. The inheritance version — class AuditedRepo extends JdbcRepo — can be broken by any change to JdbcRepo, including one that adds an internal call between two methods you overrode.

Overloading is not overriding

  • Overloading: same name, different parameter types, resolved at compile time from the static types. Two methods, unrelated at runtime.
  • Overriding: same signature in a subtype, resolved at runtime from the actual object. One method, polymorphic.

The consequence people trip on: a List<String> variable holding an ArrayList calls the overload chosen for List, and the override defined by ArrayList. Types decide overloads; objects decide overrides.

static belongs to the class

A static member exists once, on the class, and is reachable without an instance — so it cannot see instance fields and cannot be overridden (it can be hidden, which looks like overriding and is not). Static mutable state is shared by every thread in the process, which makes it the quietest way to introduce a race.

And the constructor rule that catches people: a class with no constructor gets an implicit no-arg one. Declare any constructor and that free one disappears — which is why adding a constructor with arguments breaks framework code that was calling new Thing() reflectively.

Checked or unchecked is a question about the caller

The mechanical difference is that checked exceptions must be declared or caught. The design question is better: can the caller do something useful about it?

Situation Kind
Malformed input from a user checked, or a validation result
A remote call timed out and retrying might help checked
A required config value is missing at boot unchecked — fail fast
A bug: null where the code guarantees non-null unchecked

If the caller has no meaningful response, a checked exception just forces catch (Exception e) { throw new RuntimeException(e); } at every level, which loses nothing but adds noise. Wrapping is fine — as long as the cause is kept, because a stack trace without the original cause is a lost afternoon.

finally runs, and can eat your return

int f() {
try { return 1; }
finally { System.out.println("always"); } // prints, then returns 1
}

finally runs on normal return, on exception, and on break/continue. It does not run if the JVM exits. And a return inside finally replaces the value or exception from try — swallowing the original failure silently. Never return from finally.

Try-with-resources is the better tool and closes in reverse order of declaration, which matters when the second resource depends on the first:

try (Connection c = open(); Statement s = c.createStatement()) {
...
} // s closed first, then c

NullPointerException is a fact, not a mystery

It means a member was accessed on null. On modern JVMs the message names the expression — helpful enough that turning helpful NPE messages off is rarely worth it. The fix is almost never a null check at the point of the throw; it is finding which boundary allowed the null in and stopping it there.


More on error handling at the API boundary: Surviving Production Java.