Skip to content
Laviyx Learn

Try

Java course menu

Java Polymorphism: Dynamic Dispatch, Upcasting, instanceof and Casting

Learn polymorphism in Java: treating subclass objects through a superclass or interface type, dynamic method dispatch, upcasting and downcasting, instanceof and pattern matching, ClassCastException, and why polymorphism removes long if-else chains.

Intermediate12 min readUpdated
On this page

Polymorphism ("many forms") means one piece of code can work with objects of different classes through a common type, and each object still behaves in its own way. A method that pays an Employee can pay managers, interns and contractors without knowing which is which.

Dynamic dispatch#

In Java, when you call an instance method, the JVM picks the implementation from the object's actual class at run time, not from the declared type of the variable:

Java
class Animal {
    String sound() { return "..."; }
}

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

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

public class Main {
    static void makeNoise(Animal a) {
        System.out.println(a.sound());
    }

    public static void main(String[] args) {
        makeNoise(new Dog());
        makeNoise(new Cat());
        makeNoise(new Animal());
    }
}
Output
Woof
Meow
...

makeNoise was written once, for Animal, and works for every current and future subclass. Unlike C++, Java methods are polymorphic by default; there is no virtual keyword.

A list of different objects#

The classic use: one collection, many kinds of objects, one loop:

Java
import java.util.List;

class Shape {
    double area() { return 0; }
    String name() { return "shape"; }
}

class Circle extends Shape {
    private final double r;
    Circle(double r) { this.r = r; }
    @Override double area() { return Math.PI * r * r; }
    @Override String name() { return "circle"; }
}

class Rectangle extends Shape {
    private final double w;
    private final double h;
    Rectangle(double w, double h) { this.w = w; this.h = h; }
    @Override double area() { return w * h; }
    @Override String name() { return "rectangle"; }
}

public class Main {
    public static void main(String[] args) {
        List<Shape> shapes = List.of(new Circle(1), new Rectangle(3, 4), new Circle(2));

        double total = 0;
        for (Shape s : shapes) {
            System.out.printf("%s: %.2f%n", s.name(), s.area());
            total += s.area();
        }
        System.out.printf("Total: %.2f%n", total);
    }
}
Output
circle: 3.14
rectangle: 12.00
circle: 12.57
Total: 27.71

Adding a Triangle later needs only a new class; the loop does not change. Without polymorphism you would need an if (shape is a circle) ... else if (rectangle) ... chain that must be edited for every new shape. In real code, Shape would be abstract or an interface; see the next two lessons.

Upcasting#

Treating a subclass object as its superclass type is upcasting. It is always safe and automatic:

Java
Animal a = new Dog();   // upcast: a Dog is an Animal

Through a, you can only call methods that Animal declares, even though the object is a Dog. The declared type decides which methods you may call; the actual object decides which implementation runs.

Downcasting and instanceof#

Going back from the general type to the specific one is downcasting. It needs an explicit cast, and fails at run time with a ClassCastException if the object is not really of that type. Check with instanceof first. Since Java 16, pattern matching combines the check and the cast:

Java
import java.util.List;

class Animal {}

class Dog extends Animal {
    void fetch() { System.out.println("fetching the ball"); }
}

class Cat extends Animal {}

public class Main {
    public static void main(String[] args) {
        List<Animal> pets = List.of(new Cat(), new Dog());

        for (Animal pet : pets) {
            if (pet instanceof Dog dog) {   // test and cast in one step
                dog.fetch();
            } else {
                System.out.println("not a dog");
            }
        }

        Animal a = new Cat();
        try {
            Dog d = (Dog) a;               // a Cat is not a Dog
            d.fetch();
        } catch (ClassCastException e) {
            System.out.println("ClassCastException: a Cat cannot be cast to Dog");
        }
    }
}
Output
not a dog
fetching the ball
ClassCastException: a Cat cannot be cast to Dog

If you find yourself writing many instanceof checks, the behaviour probably belongs in an overridden method instead.

Polymorphism with Object methods#

toString, equals and hashCode are polymorphic too. System.out.println(x) calls x.toString(), which runs the object's own override:

Java
import java.util.List;

class Ticket {
    private final int number;
    Ticket(int number) { this.number = number; }

    @Override
    public String toString() { return "Ticket #" + number; }
}

public class Main {
    public static void main(String[] args) {
        List<Object> things = List.of(new Ticket(7), "text", 42, 3.5);
        for (Object o : things) {
            System.out.println(o + " (" + o.getClass().getSimpleName() + ")");
        }
    }
}
Output
Ticket #7 (Ticket)
text (String)
42 (Integer)
3.5 (Double)

Overloading is not polymorphism (at run time)#

Overloaded methods are chosen at compile time from the declared types of the arguments, not from the run-time object:

Java
class Animal {}
class Dog extends Animal {}

public class Main {
    static String describe(Animal a) { return "an animal"; }
    static String describe(Dog d) { return "a dog"; }

    public static void main(String[] args) {
        Animal pet = new Dog();
        System.out.println(describe(pet));        // declared type is Animal
        System.out.println(describe(new Dog()));
    }
}
Output
an animal
a dog

Only overriding dispatches on the actual object.

Common mistakes#

Practice#

Practice exercise

Create a superclass Notification with a method send(String message) that returns a String, and subclasses EmailNotification and SmsNotification that return Email: ... and SMS: .... Put one of each in a List<Notification> and send "Your order has shipped" through all of them.

Show solution
Java
import java.util.List;

class Notification {
    String send(String message) {
        return "Generic: " + message;
    }
}

class EmailNotification extends Notification {
    @Override
    String send(String message) {
        return "Email: " + message;
    }
}

class SmsNotification extends Notification {
    @Override
    String send(String message) {
        return "SMS: " + message;
    }
}

public class Notify {
    public static void main(String[] args) {
        List<Notification> channels = List.of(new EmailNotification(), new SmsNotification());
        for (Notification channel : channels) {
            System.out.println(channel.send("Your order has shipped"));
        }
    }
}
Output
Email: Your order has shipped
SMS: Your order has shipped

Key takeaway#

Finished this lesson?