Java Polymorphism

Java Polymorphism

Polymorphism means "many forms", and it occurs when we have many classes that are related to each other by inheritance.

Like we specified in the previous chapter; Inheritance lets us inherit attributes and methods from another class. Polymorphism uses those methods to perform different tasks. This allows us to perform a single action in different ways.

For example, think of a superclass called Animal that has a method called animalSound(). Subclasses of Animals could be Pigs, Cats, Dogs, Birds - And they also have their own implementation of an animal sound (the pig oinks, and the cat meows, etc.).


Why Use Polymorphism?

Polymorphism is useful for code reusability: reuse attributes and methods of an existing class when you create a new class. It also allows you to treat objects of different classes in a uniform way.


Polymorphism Example

In this example, we have a base class Animal and two subclasses, Pig and Dog. They all have a method animalSound(), but it is implemented differently in each subclass. This is called method overriding.

Polymorphism Example

class Animal {
  public void animalSound() {
    System.out.println("The animal makes a sound");
  }
}

class Pig extends Animal { // Method overriding public void animalSound() { System.out.println("The pig says: wee wee"); } }

class Dog extends Animal { // Method overriding public void animalSound() { System.out.println("The dog says: bow wow"); } }

class Main { public static void main(String[] args) { Animal myAnimal = new Animal(); // Create a Animal object Animal myPig = new Pig(); // Create a Pig object Animal myDog = new Dog(); // Create a Dog object myAnimal.animalSound(); myPig.animalSound(); myDog.animalSound(); } }

Notice that even though myPig and myDog are declared as type Animal, the Java Virtual Machine (JVM) knows their actual object type at runtime (Pig and Dog) and calls the correct overridden method. This is called runtime polymorphism or dynamic method dispatch.


Deep Dive: Compile-Time vs. Runtime Polymorphism

Java actually supports two distinct types of polymorphism:

  1. Compile-Time Polymorphism (Static Binding): This is achieved through Method Overloading. The Java compiler determines which method to call at compile time based on the method signature (the number and type of arguments you pass in).
  2. Runtime Polymorphism (Dynamic Binding): This is achieved through Method Overriding. The JVM determines which method to execute at runtime based on the actual object type in memory, not the reference variable's type. This is what we saw in the Animal example above!

Upcasting and Downcasting

When working with polymorphism, you will frequently encounter the terms "upcasting" and "downcasting."

Upcasting (Automatic and Safe)

Upcasting is casting a child object to a parent reference. Java does this automatically and safely. It is what makes polymorphism possible.

Syntax:

// Upcasting: Dog is automatically cast to an Animal reference
Animal myPet = new Dog();

Downcasting (Manual and Risky)

If you need to access those child-specific methods, you must downcast the parent reference back to the child type. You must do this manually using parentheses.

Syntax:

Animal myPet = new Dog();

// Downcasting: Manually casting the Animal reference back to a Dog Dog myDog = (Dog) myPet; myDog.fetch(); // Now we can call Dog-specific methods!


The instanceof Operator

To prevent ClassCastException crashes when downcasting, you should always check the actual object type first using the instanceof operator.

Safe Downcasting with instanceof

class Animal {}
class Dog extends Animal {
  public void fetch() { System.out.println("Fetching a stick!"); }
}
public class Main {
  public static void main(String[] args) {
    Animal myPet = new Dog(); // Upcast
    // Safe Downcasting
    if (myPet instanceof Dog) {
      Dog myDog = (Dog) myPet;
      myDog.fetch();
    } else {
      System.out.println("This animal is not a dog.");
    }
  }
}

✨ Modern Java Feature (Java 16+): Pattern Matching for instanceof allows you to declare the cast variable directly inside the if statement, saving you a line of code!

if (myPet instanceof Dog myDog) { myDog.fetch(); }


Advanced Notes: Polymorphism


Exercise

?

What concept allows a subclass to provide a specific implementation of a method already provided by its parent class?