iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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 { }

Test yourself

Q1. A subclass declares its parent with…
Q2. A class can extend…
Q3. A class can implement…

Discussion

Loading…