Polymorphism
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.
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.