Skip to content
Part IA Michaelmas Term

Subtype Polymorphism

What Is Subtype Polymorphism?

Subtype polymorphism (also called inclusion polymorphism or dynamic polymorphism) is the ability for code written against a supertype to operate uniformly over any of its subtypes, with the actual behaviour determined by the object’s true runtime type.

The word “polymorphism” comes from Greek: poly (many) + morph (form). A variable of type T can take on many forms — any object whose class is a subtype of T.

The Mechanism

Two things must happen for subtype polymorphism to work:

  1. Compile-time check: the compiler verifies that the method being called exists on the declared type of the reference. If it does not, the code does not compile.
  2. Runtime dispatch: the JVM finds the correct implementation to execute by looking at the runtime type of the object, using its virtual method table (vtable).

This is why you can write:

List<Shape> shapes = Arrays.asList(new Circle(5), new Square(3), new Triangle(4, 6));
for (Shape s : shapes) {
    System.out.println(s.area());
}

The loop body is written against Shape. The compiler checks that area() exists on Shape (it does). At runtime, s.area() dispatches to Circle.area(), Square.area(), and Triangle.area() respectively — each shape computes its own area.

Reference Type vs Object Type

Every expression in Java has two types:

TypeDeterminesSet by
Declared type (compile-time type, static type)Which methods can be calledThe type written in the variable declaration
Runtime type (dynamic type, actual type)Which implementation runsThe class passed to new when the object was created
Object o = new String("hello");
o.toString();   // OK — toString() is on Object (declared type allows it)
o.length();     // Compile error — length() is on String, not Object (declared type forbids it)

The declared type is an over-approximation of the possible runtime types. The runtime type is always a subtype (same or more specific) of the declared type.

The Open-Closed Principle Connection

Subtype polymorphism is the primary mechanism OOP provides for satisfying the Open-Closed Principle (OCP). OCP states that code should be open for extension but closed for modification. With subtype polymorphism:

  • A method written against a supertype (Shape) is closed for modification — you do not need to edit it when new shapes are added.
  • But it is open for extension — you can add a new Hexagon class and pass it to the same method, and it works without changing the method.
void printAreas(Shape[] shapes) {
    for (Shape s : shapes) {
        System.out.println(s.area());
    }
}

This method will work with any new Shape subclass — present or future — as long as it correctly implements area(). No instanceof checks, no switch statements, no modification required.

The Violation Pattern

The OCP violation that polymorphism fixes:

void printAreas(Object[] shapes) {
    for (Object o : shapes) {
        if (o instanceof Circle c) {
            System.out.println(Math.PI * c.radius * c.radius);
        } else if (o instanceof Square s) {
            System.out.println(s.side * s.side);
        } else if (o instanceof Triangle t) {
            System.out.println(0.5 * t.base * t.height);
        }
        // MUST BE EDITED every time a new shape is added
    }
}

Every new shape requires finding and editing this method (and every similar method). This is a maintenance disaster and a bug magnet.

Polymorphism Through Arrays and Collections

Polymorphism works with arrays and generics. The key point: an array of a supertype can hold objects of any subtype:

Animal[] zoo = new Animal[3];
zoo[0] = new Dog();
zoo[1] = new Cat();
zoo[2] = new Bird();

for (Animal a : zoo) {
    a.speak();  // dynamic dispatch: each animal speaks in its own way
}

With generics, use the bounded wildcard or interface type:

List<Animal> zoo = new ArrayList<>();
zoo.add(new Dog());
zoo.add(new Cat());

Polymorphism and Method Parameters

A method parameter typed to a supertype accepts any subtype:

void feed(Animal a) {
    a.eat();
}

feed(new Dog());
feed(new Cat());
feed(new Bird());

This is the fundamental technique for writing generic, extensible code. The feed method does not know or care what specific animal it receives — it only cares that the animal can eat().

The Cost: Runtime Overhead

Dynamic dispatch adds a small runtime overhead compared to a direct function call. The JVM must:

  1. Follow the reference to the object.
  2. Read the object’s class pointer.
  3. Look up the method in the class’s vtable.
  4. Jump to the method implementation.

Modern JVMs mitigate this cost with inline caching (remembering the most recent target type and inlining the common case) and speculative optimisation. In practice, the overhead is negligible for most applications, and the design benefits of polymorphism far outweigh the cost.