Skip to main content
Java · lesson 15 of 66
Open the Java page →
Java beginner Lesson 15 of 66

Polymorphism in Java

See Java polymorphism run: overriding and dynamic dispatch, coding to interfaces, why fields and static methods don't dispatch, pattern-matching switch.

By SkillByExample · Updated

Polymorphism means one piece of code works with many types. In Java it mostly means dynamic dispatch: a call through a supertype reference runs the method of the object’s actual class. Every example is a single file run with the JDK 21 source launcher (java File.java).

Overriding and dynamic dispatch

import java.util.List;

public class Dispatch {
    static class Animal {
        String speak() { return "..."; }
        String intro() { return getClass().getSimpleName() + " says " + speak(); }
    }

    static class Dog extends Animal {
        @Override String speak() { return "Woof"; }
    }

    static class Cat extends Animal {
        @Override String speak() { return "Meow"; }
    }

    public static void main(String[] args) {
        List<Animal> animals = List.of(new Animal(), new Dog(), new Cat());
        for (Animal a : animals) {
            System.out.println(a.intro());
        }
    }
}
$ java Dispatch.java
Animal says ...
Dog says Woof
Cat says Meow

The loop variable is declared Animal, yet each call runs the subclass’s speak. Even inside Animal.intro(), the call to speak() dispatches on the real object. That is the core trick: intro was written once and works for subclasses that did not exist when it was written.

@Override catches mistakes

Without @Override, a typo creates a new method instead of overriding one.

public class OverrideCheck {
    static class Shape {
        double area() { return 0; }
    }

    static class Square extends Shape {
        double side = 3;
        double Area() { return side * side; }       // capital A: a new method
    }

    static class Circle extends Shape {
        double r = 1;
        @Override double area() { return Math.PI * r * r; }
    }

    public static void main(String[] args) {
        Shape[] shapes = {new Square(), new Circle()};
        for (Shape s : shapes) {
            System.out.printf("%s %.2f%n", s.getClass().getSimpleName(), s.area());
        }
    }
}
$ java OverrideCheck.java
Square 0.00
Circle 3.14

Square reports 0.00 because Area() never overrode area(). Put @Override on it and the compiler rejects it with “method does not override or implement a method from a supertype”.

Programming to an interface

Write methods against an interface and they accept every implementation, present or future.

import java.util.List;

public class Payments {
    interface PaymentMethod {
        String pay(double amount);
    }

    record Card(String last4) implements PaymentMethod {
        public String pay(double amount) { return "charged %.2f to card *%s".formatted(amount, last4); }
    }

    record Wallet(String email) implements PaymentMethod {
        public String pay(double amount) { return "sent %.2f from %s".formatted(amount, email); }
    }

    static void checkout(PaymentMethod method, double total) {
        System.out.println(method.pay(total));
    }

    public static void main(String[] args) {
        checkout(new Card("4242"), 19.99);
        checkout(new Wallet("ada@example.com"), 5);
        PaymentMethod free = amount -> "nothing to pay for " + amount;   // a lambda works too
        checkout(free, 0);
    }
}
$ java Payments.java
charged 19.99 to card *4242
sent 5.00 from ada@example.com
nothing to pay for 0.0

checkout depends only on the pay contract. Adding a new payment type means a new class, not an edit to checkout. Because PaymentMethod has a single abstract method, a lambda can implement it.

What does not dispatch: fields and static methods

Only instance methods are polymorphic. Fields and static methods are chosen by the declared type.

public class Hiding {
    static class Parent {
        String name = "parent field";
        static String kind() { return "Parent.kind"; }
        String who() { return "Parent.who"; }
    }

    static class Child extends Parent {
        String name = "child field";                 // hides, does not override
        static String kind() { return "Child.kind"; }
        @Override String who() { return "Child.who"; }
    }

    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.who());
        System.out.println(p.name);
        System.out.println(((Child) p).name);
        System.out.println(Parent.kind() + " / " + Child.kind());
    }
}
$ java Hiding.java
Child.who
parent field
child field
Parent.kind / Child.kind

p.who() dispatches to Child, but p.name reads Parent’s field because p is declared Parent. Keep fields private and expose them through methods, and this confusion cannot arise.

Overloading is chosen at compile time

Overloads are picked from declared types, which makes them look polymorphic until they are not.

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

    static String feed(Animal a) { return "feeding an animal that says " + a.sound(); }
    static String feed(Dog d)    { return "feeding a dog that says " + d.sound(); }

    public static void main(String[] args) {
        Animal rex = new Dog();
        System.out.println(feed(rex));
        System.out.println(feed((Dog) rex));
    }
}
$ java StaticVsDynamic.java
feeding an animal that says woof
feeding a dog that says woof

The compiler chose feed(Animal) because rex is declared Animal — yet inside it, a.sound() still dispatched to Dog. Overload selection is static, override selection is dynamic. The method overloading lesson covers the resolution rules.

Sealed types and pattern-matching switch

Java 21 adds another style: a sealed interface lists its implementations, and a switch over it must handle every one.

public class Sealed {
    sealed interface Shape permits Circle, Rect, Triangle {}
    record Circle(double r) implements Shape {}
    record Rect(double w, double h) implements Shape {}
    record Triangle(double b, double h) implements Shape {}

    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.r() * c.r();
            case Rect r when r.w() == r.h() -> r.w() * r.w();      // squares
            case Rect r -> r.w() * r.h();
            case Triangle t -> 0.5 * t.b() * t.h();
        };
    }

    public static void main(String[] args) {
        for (Shape s : new Shape[] {new Circle(1), new Rect(2, 2), new Rect(2, 5), new Triangle(3, 4)}) {
            System.out.printf("%-30s %.2f%n", s, area(s));
        }
    }
}
$ java Sealed.java
Circle[r=1.0]                  3.14
Rect[w=2.0, h=2.0]             4.00
Rect[w=2.0, h=5.0]             10.00
Triangle[b=3.0, h=4.0]         6.00

No default branch is needed: the compiler knows the three permitted types, and if a fourth were added to permits, this switch would stop compiling until it was handled. Overriding puts the behaviour inside each type; a sealed switch keeps one operation in one place.

Covariant return types

An override may narrow its return type to a subclass of the original.

public class Covariant {
    static class Animal {
        Animal reproduce() { return new Animal(); }
    }

    static class Dog extends Animal {
        @Override Dog reproduce() { return new Dog(); }   // Dog, not Animal
        void wag() { System.out.println("puppy wags"); }
    }

    public static void main(String[] args) {
        Dog puppy = new Dog().reproduce();                  // no cast needed
        puppy.wag();
        Animal a = new Dog();
        System.out.println(a.reproduce().getClass().getSimpleName());
    }
}
$ java Covariant.java
puppy wags
Dog

Callers that know they have a Dog get a Dog back without casting, while code holding an Animal still works unchanged. clone() overrides commonly use this.

Practice

  1. Add a Cow to Dispatch that says "Moo", without editing Animal or the loop body.
Solution
import java.util.List;

public class AddCow {
    static class Animal {
        String speak() { return "..."; }
        String intro() { return getClass().getSimpleName() + " says " + speak(); }
    }

    static class Dog extends Animal { @Override String speak() { return "Woof"; } }
    static class Cow extends Animal { @Override String speak() { return "Moo"; } }

    public static void main(String[] args) {
        for (Animal a : List.of(new Dog(), new Cow())) System.out.println(a.intro());
    }
}
$ java AddCow.java
Dog says Woof
Cow says Moo

Extending the hierarchy needed no change to existing code — the open/closed principle in action.

  1. Fix OverrideCheck so Square really overrides area(), and confirm with @Override.
Solution
public class OverrideFixed {
    static class Shape { double area() { return 0; } }

    static class Square extends Shape {
        double side = 3;
        @Override double area() { return side * side; }
    }

    public static void main(String[] args) {
        Shape s = new Square();
        System.out.println(s.area());
    }
}
$ java OverrideFixed.java
9.0

With the name corrected, @Override compiles and the call through Shape reaches Square.

  1. Add a Hexagon(double side) record to the sealed Shape hierarchy (area is 3 * sqrt(3) / 2 * side * side). What does the compiler say before you update area?
Solution
public class SealedHex {
    sealed interface Shape permits Circle, Hexagon {}
    record Circle(double r) implements Shape {}
    record Hexagon(double side) implements Shape {}

    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.r() * c.r();
        };
    }

    public static void main(String[] args) {
        System.out.println(area(new Hexagon(1)));
    }
}
$ java SealedHex.java
SealedHex.java:7: error: the switch expression does not cover all possible input values
        return switch (s) {
               ^
1 error
error: compilation failed

Adding the missing case fixes it:

public class SealedHexFixed {
    sealed interface Shape permits Circle, Hexagon {}
    record Circle(double r) implements Shape {}
    record Hexagon(double side) implements Shape {}

    static double area(Shape s) {
        return switch (s) {
            case Circle c -> Math.PI * c.r() * c.r();
            case Hexagon h -> 3 * Math.sqrt(3) / 2 * h.side() * h.side();
        };
    }

    public static void main(String[] args) {
        System.out.printf("%.3f%n", area(new Hexagon(1)));
    }
}
$ java SealedHexFixed.java
2.598

Exhaustiveness checking turns “forgot to handle the new type” from a runtime bug into a compile error.

  1. Write a Notifier interface with send(String msg) and two implementations, Email and Sms (SMS truncates to 10 characters). Send the same message through a List<Notifier>.
Solution
import java.util.List;

public class Notify {
    interface Notifier { String send(String msg); }

    static class Email implements Notifier {
        public String send(String msg) { return "email: " + msg; }
    }

    static class Sms implements Notifier {
        public String send(String msg) { return "sms: " + (msg.length() > 10 ? msg.substring(0, 10) : msg); }
    }

    public static void main(String[] args) {
        for (Notifier n : List.of(new Email(), new Sms())) {
            System.out.println(n.send("Your order has shipped"));
        }
    }
}
$ java Notify.java
email: Your order has shipped
sms: Your order

The loop knows only Notifier; each object decides how to deliver the message.

Frequently Asked Questions

What is the difference between method overriding and method overloading?
Overriding is a subclass replacing an inherited method with the same name and parameters; the JVM picks the version from the object's actual class at runtime. Overloading is several methods with the same name but different parameter lists; the compiler picks one from the declared argument types at compile time.
What is dynamic dispatch?
Dynamic dispatch is how the JVM chooses which overridden method to run: it looks at the class of the object a reference points to, not the declared type of the reference. An Animal variable holding a Dog therefore runs Dog's speak method.
Can static methods or fields be polymorphic?
No. Static methods and fields are resolved at compile time from the declared type. A subclass static method with the same signature hides the parent's rather than overriding it, and a subclass field with the same name hides the parent's field. Only instance methods take part in dynamic dispatch.
What does the @Override annotation do?
@Override asks the compiler to check that the method really overrides a method from a superclass or interface. If the name or parameter types are slightly wrong, compilation fails instead of silently creating a new, unrelated method that is never called polymorphically.
When should I use a sealed interface with pattern matching instead of overriding?
Overriding suits behaviour that belongs to each type and when new types will be added often. A sealed interface with a switch over its permitted types suits operations defined outside the types, such as formatting or pricing, because the compiler checks that every permitted type is handled.