Inheritance
Java’s inheritance model is single inheritance for classes plus multiple inheritance for interfaces (now including default methods). Modern Java leans heavily on composition, records, and sealed hierarchies — deep class hierarchies are a maintenance smell, not a goal.
extends, abstract, interface, sealed
EXAMPLE
// 1) Single class inheritance
public class Animal {
private final String name;
public Animal(String name) { this.name = name; }
public String name() { return name; }
public String sound() { return "…"; }
@Override public String toString() { return getClass().getSimpleName() + " " + name; }
}
public class Dog extends Animal {
public Dog(String name) { super(name); } // call base constructor
@Override public String sound() { return "woof"; }
}
public class Puppy extends Dog {
public Puppy(String name) { super(name); }
@Override public String sound() { return super.sound() + " (small)"; }
}
// 2) abstract — must be subclassed
public abstract class Shape {
public abstract double area();
public String describe() { return getClass().getSimpleName() + " area=" + area(); }
}
public class Circle extends Shape {
private final double r;
public Circle(double r) { this.r = r; }
@Override public double area() { return Math.PI * r * r; }
}
// 3) final — block further subclassing OR overriding
public final class Sealed { /* can't be subclassed */ }
public class Base {
public final void critical() { /* subclasses can't change this */ }
}
// 4) Interfaces — multiple inheritance for type + behaviour
public interface Drawable {
void draw();
default void clear() { /* default impl since Java 8 */ }
static int defaultSize() { return 10; }
}
public interface Movable {
void moveTo(int x, int y);
}
public class Widget implements Drawable, Movable {
@Override public void draw() { System.out.println("draw"); }
@Override public void moveTo(int x, int y) { /* … */ }
}
// 5) Diamond resolution — explicit pick
public interface A { default String name() { return "A"; } }
public interface B { default String name() { return "B"; } }
public class C implements A, B {
@Override public String name() { return A.super.name(); } // pick one explicitly
}
// 6) Sealed classes (Java 17+) — restrict who can extend
public sealed interface Result<T> permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(Throwable error) implements Result<T> {}
// Use with pattern matching in switch (Java 21+)
static String describe(Result<?> r) {
return switch (r) {
case Success<?> s -> "ok: " + s.value();
case Failure<?> f -> "err: " + f.error().getMessage();
};
}
// The compiler verifies exhaustiveness — adding a new permitted type breaks all switches until you handle it.
// 7) Records + inheritance
public sealed interface Event permits Created, Updated, Deleted {}
public record Created(String id, long at) implements Event {}
public record Updated(String id, long at, String field) implements Event {}
public record Deleted(String id, long at) implements Event {}
// 8) Visibility modifiers
// public — anywhere
// protected — same package + subclasses
// (default) — same package
// private — same class only
// 9) super and overriding
public class Logger {
public void log(String msg) { System.out.println("[INFO] " + msg); }
}
public class Timestamped extends Logger {
@Override public void log(String msg) {
super.log("[" + java.time.Instant.now() + "] " + msg);
}
}
// 10) Constructors run base-to-derived; call super(...) explicitly when base has args
public class Employee extends Person {
private final String department;
public Employee(String name, int age, String department) {
super(name, age); // first statement
this.department = department;
}
}
// 11) The 'instanceof' pattern (Java 16+) + pattern matching
public String describe(Object o) {
if (o instanceof Dog d) return "dog: " + d.name();
if (o instanceof Circle c) return "circle r=" + c.area();
return "unknown";
}
// 12) Generics + bounded type parameters
public class Cage<T extends Animal> {
private final T animal;
public Cage(T animal) { this.animal = animal; }
public String sound() { return animal.sound(); }
}
Cage<Dog> d = new Cage<>(new Dog("Rex"));
// 13) Liskov red flags
// • Override that throws UnsupportedOperationException → caller can't substitute safely
// • Subclass tightens preconditions (rejects inputs the base accepts) → LSP violation
// • Square : Rectangle with mutable width/height → classic LSP violation
// • Need 'if (obj instanceof X)' after a polymorphic call → polymorphism failing
// • Subclass adds public state needed by every caller → leaky base abstraction
// 14) Prefer composition
public class Cache {
private final Map<String, Object> data = new HashMap<>();
public Object get(String k) { return data.get(k); }
}
public class TimedCache {
private final Cache inner = new Cache(); // composition — easier to evolve
public Object get(String k) {
long start = System.nanoTime();
try { return inner.get(k); }
finally { log.info("cache get took {} ns", System.nanoTime() - start); }
}
}
// 15) Common bugs
// • Overriding equals without hashCode → broken in HashMap / HashSet
// • Calling a virtual method from a constructor → subclass code runs on a half-built object
// • Subclassing a concrete class to override one method → consider an interface + composition
// • Using 'new' to hide a base method (Java doesn't have 'new', but TypeScript-trained devs reach for it) — Java has no equivalent; use @Override
// • Missing @Override on intended overrides → typo creates a fresh method, silently breaking polymorphism
// • Constructors with side effects relying on subclass state → run base first, subclass state is null
// • Inheritance for code reuse only → fragile base class problem; prefer composition
// • Forgetting that interfaces can have static + default methods now → unnecessary abstract classes
Why it matters
Modern Java leans on records and sealed interfaces, and you should too: records give you correct equals/hashCode/toString automatically, and a sealed hierarchy lets pattern matching cover every case at compile time. Reach for inheritance only when the IS-A relationship is stable across time; otherwise compose.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
class Animal { String speak() { return "…"; } }
class Dog extends Animal { @Override String speak() { return "woof"; } }
Try it Yourself »
Exercise
Inherit from Animal.
class Dog
Animal { }
Seven letters.
Discussion
Loading…