EMZETT.
Login

Polymorphism

In short: An object of a subclass can be used anywhere the superclass is expected — on a method call, the actual object type decides at runtime which implementation runs.

In more detail: Example: a variable of type Animal can hold a Dog object at runtime; calling makeSound() runs the overridden Dog version, even though the variable’s type is Animal (“dynamic dispatch”). This allows, for example, a list List<Animal> with mixed subclasses, where every element shows its own behaviour.

In Depth

class Animal { void makeSound() { System.out.println("..."); } }
class Dog extends Animal { void makeSound() { System.out.println("Woof"); } }
class Cat extends Animal { void makeSound() { System.out.println("Meow"); } }
 
List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
animals.add(new Cat());
animals.add(new Animal());
 
for (Animal a : animals) {
    a.makeSound(); // "Woof", "Meow", "..." - each the correct, overridden version
}

This kind of polymorphism (“runtime polymorphism”/“dynamic dispatch”) differs fundamentally from static overloading (see Overloading): which makeSound() implementation actually runs is decided by the JVM only AT RUNTIME based on the actual object type (not the variable type Animal). This is exactly what makes the list List<Animal> with mixed subclasses usable at all: calling code (the for loop) doesn’t need to know whether a specific element is a Dog or a Cat, but relies on EVERY Animal subclass delivering its own matching makeSound() version. This is the practical basis for, for example, plugin systems or interchangeable strategies (strategy pattern), where new code (another animal species) can be added without touching the existing loop code.

Polymorphism via interfaces instead of inheritance

Polymorphism works just as well via interfaces instead of concrete inheritance — even more flexibly, because a class can implement several interfaces but inherit from only ONE superclass:

interface Drivable { void drive(); }
 
class Car implements Drivable { public void drive() { System.out.println("Drives on wheels"); } }
class Boat implements Drivable { public void drive() { System.out.println("Drives on water"); } }
 
List<Drivable> vehicles = List.of(new Car(), new Boat());
for (Drivable v : vehicles) {
    v.drive(); // each the matching implementation, independent of shared inheritance
}

instanceof — recognising the actual type when needed

Polymorphism is the normal case, but sometimes code does need to know which CONCRETE subtype it’s dealing with — that’s what instanceof is for, since Java 16 with direct type casting (“pattern matching”):

for (Animal a : animals) {
    if (a instanceof Dog dog) { // checks AND casts in one step
        System.out.println(dog.bark()); // a Dog-specific method that Animal doesn't have
    }
}

Frequent instanceof chains over all subclasses, however, are often considered a sign that polymorphism (one shared, overridden method) would be the more elegant solution — otherwise every new subclass would require an adjustment to EVERY instanceof chain throughout the entire program, instead of simply implementing the shared method itself.

See also: Inheritance, Interface, OOP