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

Polymorphism

Polymorphism lets one call site dispatch to the right implementation at runtime. Java offers it via inheritance (extends + virtual methods), interfaces (multiple-inheritance of behaviour), and pattern matching on sealed hierarchies (Java 21+).

virtual dispatch, interfaces, sealed

EXAMPLE
// 1) Classic virtual dispatch
public class Animal {
    public String sound() { return "..."; }
    @Override public String toString() { return getClass().getSimpleName() + "(" + sound() + ")"; }
}

public class Dog extends Animal {
    @Override public String sound() { return "woof"; }
}

public class Cat extends Animal {
    @Override public String sound() { return "meow"; }
}

Animal a = new Dog();
System.out.println(a.sound());     // 'woof' — runtime type wins

// 2) Generic polymorphism + List<Animal>
List<Animal> zoo = List.of(new Dog(), new Cat(), new Dog());
zoo.forEach(animal -> System.out.println(animal.sound()));
// Output: woof, meow, woof

// 3) Interface polymorphism — usually preferred over deep class hierarchies
public interface Payment {
    Receipt charge(long amountCents);
}

public class CreditCard implements Payment {
    @Override public Receipt charge(long amountCents) { /* ... */ return new Receipt("cc", amountCents); }
}
public class PayPal implements Payment {
    @Override public Receipt charge(long amountCents) { /* ... */ return new Receipt("pp", amountCents); }
}

public class Checkout {
    public Receipt pay(Payment method, long amountCents) {
        return method.charge(amountCents);
    }
}
// Checkout doesn't know or care which payment type.
// Adding a fourth method = one new class + one DI registration.

// 4) sealed interfaces — closed hierarchy (Java 17+)
public sealed interface Shape permits Circle, Square, Rect {}
public record Circle(double r)              implements Shape {}
public record Square(double side)            implements Shape {}
public record Rect  (double w, double h)     implements Shape {}

// Pattern matching switch (Java 21+) — exhaustive
static double area(Shape s) {
    return switch (s) {
        case Circle c   -> Math.PI * c.r() * c.r();
        case Square sq  -> sq.side() * sq.side();
        case Rect r     -> r.w() * r.h();
    };
}
// Compiler enforces all variants handled; adding a new permits without updating switches is a compile error.

// 5) Records + sealed hierarchies — the modern data shape
public sealed interface Result<T> permits Success, Failure {}
public record Success<T>(T value)      implements Result<T> {}
public record Failure<T>(Throwable err) implements Result<T> {}

static <T> T unwrap(Result<T> r) {
    return switch (r) {
        case Success<T> s -> s.value();
        case Failure<T> f -> throw new RuntimeException(f.err());
    };
}

// 6) Covariant return types (Java 5+)
public class Animal2 { public Animal2 clone()  { return new Animal2(); } }
public class Dog2 extends Animal2 { @Override public Dog2 clone() { return new Dog2(); } }
// Caller of Dog2.clone() gets Dog2, not Animal2 — strongly typed override.

// 7) Default + static methods on interfaces
public interface Logger {
    void log(LogLevel level, String msg);
    default void info(String msg)  { log(LogLevel.INFO, msg); }
    default void error(String msg) { log(LogLevel.ERROR, msg); }
    static Logger silent() { return (l, m) -> {}; }
}

// Implementers only need to provide log(); info/error come for free unless overridden.

// 8) Diamond resolution — multiple default methods
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 explicitly
}

// 9) Generic + bounded
public class Cache<K, V extends Cacheable> {
    private final Map<K, V> map = new HashMap<>();
    public V get(K k) { return map.get(k); }
}

public interface Cacheable { String cacheKey(); }

// 10) Composition over inheritance — composition uses polymorphism too
public class TimedCache {
    private final Cache<String, ?> inner;
    public TimedCache(Cache<String, ?> inner) { this.inner = inner; }
    public Object get(String k) {
        long start = System.nanoTime();
        try { return inner.get(k); }
        finally { System.out.println("took " + (System.nanoTime() - start) + "ns"); }
    }
}

// 11) Liskov red flags
// • Override throws UnsupportedOperationException — caller can't substitute safely
// • Subclass tightens preconditions — LSP violation
// • Square : Rectangle with separate setWidth/setHeight — classic LSP violation
// • Need 'if (obj instanceof X)' after a polymorphic call — polymorphism failed

// 12) Pattern matching for instanceof (Java 16+)
String describe(Object o) {
    if (o instanceof Dog d)      return "dog: " + d.sound();
    if (o instanceof Cat c)      return "cat: " + c.sound();
    return "unknown";
}

// 13) Where polymorphism is the WRONG tool
// • Branching on type AND on state — state pattern instead
// • Adding a new subtype needs N file edits — visitor pattern or sealed + switch
// • Polymorphic call in a tight loop — measure; consider sealed for inlining hints
// • Hierarchy depth >= 3 levels — almost always reconsider; flatten with composition

// 14) Common bugs
// • Missing @Override on intended override → typo creates a new method silently
// • Calling a virtual method from a constructor → subclass code runs on a half-built object
// • Adding a permits type without updating switches → compile error; embrace it
// • Mixed inheritance + interface dispatch with same method name → resolve explicitly
// • Inheritance for code reuse only → prefer composition
// • Polymorphism via 'object' + downcast → usually a missing interface
// • Forgetting to make a base class abstract → instantiable with no meaningful behaviour

Why it matters

Java polymorphism in 2025 means interfaces injected via DI, sealed hierarchies with pattern-matched switches, and inheritance reserved for abstract bases with shared concrete behaviour. Records make data variants trivial, and the compiler enforces exhaustiveness on sealed switches — safety nets old-school extends never had.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
Animal a = new Dog();
System.out.println(a.speak());  // "woof" — runtime dispatch
Try it Yourself »

Discussion

Loading…