Polymorphism

Basic Java
≈ 3 min read

One Reference Type — Different Behavior

Inheritance gives us an important capability: we can work with an object of a child class through the type of its parent. At the same time, an overridden method still runs the way it is defined in the actual object.

class Animal {
    void speak() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Woof");
    }
}

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Meow");
    }
}

public class Main {

    public static void main(String[] args) {
        Animal first = new Dog();
        Animal second = new Cat();

        first.speak();   // Woof
        second.speak();  // Meow
    }
}

The variables first and second have the type Animal, but they contain different objects. When speak() is called, Java looks at the actual object and chooses its overridden implementation. This is one of the main ideas of polymorphism: one common type can represent objects of different child classes, while the same call can produce different behavior.

Why It Is Useful

Polymorphism lets us write code that does not have to know the specific child class. For example, a method can accept any Animal object and simply call the method it knows about:

static void makeSpeak(Animal animal) {
    animal.speak();
}

Now we can pass both Dog and Cat to it. The makeSpeak() method itself does not need to change: each object runs its own implementation of speak(). This makes the code less dependent on specific classes and easier to extend with new behavior variants.

Polymorphism works together with overriding. The reference type determines which methods can be called at all, while the actual object determines which overridden implementation of a regular instance method will run.

The Reference Type and the Object Type Are Not the Same

The line Animal animal = new Dog(); contains two types at once. Animal is the reference type, while Dog is the type of the created object. This distinction is important when reading code.

Through a reference, only methods known to the reference type are available. If we add a fetch() method to Dog, this call will not compile:

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

Animal animal = new Dog();
animal.fetch();

The compiler sees the variable as Animal and that type does not have a fetch() method. But if a method exists in Animal and is overridden in Dog, the version from Dog will be selected at runtime.

Type Casting

Converting from a child type to a parent type happens automatically:

Dog dog = new Dog();
Animal animal = dog;

Casting in the opposite direction must be explicit because not every Animal object has to be a Dog:

if (animal instanceof Dog actualDog) {
    actualDog.fetch();
}

This check first makes sure that the object really is a Dog and then gives us a reference of the required type. But if code constantly has to discover the concrete class of an object, that is often a sign that the possibilities of polymorphism are not being used fully.