Polymorphism and Abstract Types in Java – Dynamic Dispatch in Action

Introduction

Polymorphism is what lets a program treat different objects the same way, without knowing their exact types at compile time. In Java, this power comes from dynamic dispatch, abstract classes, and interfaces. In this post, we’ll explore how polymorphism actually works in Java, clarify the roles of abstract classes versus interfaces, and discuss when to choose one over the other.


1. What Is Polymorphism?

The word polymorphism comes from Greek, meaning “many shapes.” In Java, it means you can write code that works on the superclass or interface type, and at runtime it executes the subclass’s or implementation’s method.

public interface Shape {
    double area();
}

public class Circle implements Shape {
    private double radius;
    public Circle(double r) { radius = r; }
    @Override
    public double area() { return Math.PI * radius * radius; }
}

public class Rectangle implements Shape {
    private double width, height;
    public Rectangle(double w, double h) { width = w; height = h; }
    @Override
    public double area() { return width * height; }
}

A method can accept any Shape:

public static void printArea(Shape s) {
    System.out.println("Area = " + s.area());
}

Even though printArea knows only about Shape, it calls the right area() implementation at runtime.

Teaching Tip:

Demonstrate polymorphism by looping over a List<Shape> containing both Circle and Rectangle objects.


2. Dynamic Dispatch Under the Hood

At compile time, the compiler checks that a call like s.area() is valid on type Shape. But at runtime, the JVM uses the object’s actual class to find the method implementation, this is dynamic dispatch (a.k.a. virtual method dispatch).

  • The method table (v-table) stored per class lets the JVM look up and invoke the right method quickly.
  • final methods and static methods bypass dynamic dispatch: the compiler binds those calls early.

Quirk:

Calling a static method on a subclass reference doesn’t use the subclass’s override, it uses the type of the reference.


3. Abstract Classes vs. Interfaces

Both let you define types that can’t be instantiated directly, but they serve different purposes:

FeatureAbstract ClassInterface
Multiple inheritanceNoYes (since Java 8 with default methods)
State (fields)Can have fieldsCannot have instance fields (only constants)
ConstructorsYesNo
Default method behaviorOverride or inherit as usualDefault methods (since Java 8)
When to use“Is-a” with shared code/statePure contract with no shared state

Example Abstract Class:

public abstract class Employee {
    protected String name;
    public Employee(String name) { this.name = name; }
    public abstract double calculatePay();
}

Example Interface:

public interface Taxable {
    double taxAmount();
    default void printTax() { System.out.println("Tax: " + taxAmount()); }
}

4. When to Choose Interfaces vs. Abstract Classes

  • Use abstract classes when:

    • You want to share code or state among related classes.
    • You need non-public members or constructor logic.
  • Use interfaces when:

    • You need a pure specification with no state.
    • You expect many unrelated classes to implement the same API.
    • You want multiple inheritance of behavior via default methods.

Teaching Tip:

Create a class hierarchy for employees to show shared code in an abstract class, then add an unrelated Taxable interface for separate responsibilities.


5. Casting and instanceof

Sometimes you know more specific information at runtime:

Shape s = getShape();
if (s instanceof Circle) {
    Circle c = (Circle) s;
    System.out.println("Radius = " + c.getRadius());
}

Use instanceof sparingly, it can be a sign you’re breaking polymorphism’s benefits.


6. Real-World Patterns

  • Strategy Pattern: Define a family of algorithms in interfaces and swap implementations at runtime.
  • Template Method Pattern: Put skeleton logic in an abstract class and let subclasses fill in specifics.

Discussing these patterns illustrates why polymorphism matters beyond basic OOP exercises.


Key Takeaways

  1. Polymorphism lets you write code against abstract types, while dynamic dispatch picks the right implementation.
  2. Abstract classes and interfaces both define types, but differ in inheritance, state, and shared code.
  3. Choose based on whether you need shared implementation (abstract class) or a pure contract (interface).
  4. Patterns like Strategy and Template Method build on polymorphism to solve real problems.

Next up: Java’s memory model and garbage collection, understand how objects live, die, and how the JVM keeps your program running smoothly.