Polymorphism (from the Greek words for "many shapes") is one of the pillars of Object-Oriented Programming (OOP). It allows us to treat objects of different child classes as if they were objects of their shared parent class, while still letting each object behave in its own unique way at runtime.

Let's look at how Java handles polymorphic method dispatch using a simple theater actor analogy.

Illustration of dynamic method lookup for parent A and child B classes
Real-World Analogy: The Theater Actor

Imagine a director writing a theater script. The script has a role called "Actor" (the Parent Class A) with directions like "give speech" (method m1()) and "take a bow" (method m2()).

When the play begins, the director hires a specific actor named "Bob" (the Child Class B) to play the role. The play unfolds like this:

  • The script says: Actor a = new Bob(); (The director refers to Bob simply as "the actor" on stage).
  • The script commands the actor to: a.m1(); ("give speech"). Even though the script is written for a generic Actor, Bob delivers his own custom lines that he rehearsed (B's overridden method m1() executes).
  • The script commands the actor to: a.m2(); ("take a bow"). Since Bob didn't prepare a custom bow, he performs the standard bow that all actors are trained to do (A's inherited method m2() executes).

Walkthrough of the Main Method Scenario

Let's trace how the program executes step-by-step from the entry point of the main method:

Step 1: Declaration with Reference Type vs. Object Type

The program declares a reference variable a of type A, but points it to an instance of class B on the heap:

A a = new B();

At compile time, the compiler only checks if class A has the method we want to call. Since class A has m1(), the code compiles successfully.

Step 2: Dynamic Method Lookup for a.m1()

Next, the program calls a.m1():

  • At runtime, the JVM looks at the actual object on the heap, which is of type B.
  • It checks if class B overrides m1(). It does!
  • Thus, it invokes B's version of the method, printing: **B m1()**.
Step 3: Direct Reference Execution for b.m1()

Then, the program declares a reference and object both of child type B:

B b = new B();
b.m1();

Since both the reference and the object are of type B, the JVM directly calls B's m1() method, printing: **B m1()**.

Important Concept: Variables are NOT Polymorphic!

Notice that Parent Class A declares protected int x = 10;. If Child Class B were to declare its own int x = 20;, variables do not override each other (this is called variable hiding). If we access a.x, it would print 10 because field access is resolved at compile time based on the reference type (A), not the actual object type!

Java Implementation

Below is the complete Java code demonstrating polymorphism and class method inheritance:

package io.practise.accolite;
 
public class InheritanceChecker {
    public static void main(String[] args) {
 
        A a = new B();
        a.m1(); // Prints: B m1()
 
        B b = new B();
        b.m1(); // Prints: B m1()
    }
}
 
class A {
    protected int x = 10;
 
    public void m1() {
        System.out.println("A m1()");
    }
 
    public void m2() {
        System.out.println("A m2()");
    }
}
 
class B extends A {
    @Override
    public void m1() {
        System.out.println("B m1()");
    }
}

Conclusion

Polymorphism allows our code to be flexible and extensible. By writing code that references the parent class, we can easily swap in new child classes in the future without changing our main logic. The JVM will automatically look up the correct overridden method implementation at runtime!