An abstract illustration: a single flowing line threading through many different geometric shapes — one essence taking many forms.

What is Polymorphism?

Polymorphism is the property of having many forms — and, fittingly, the word itself takes many forms, notably in biology and computer science. We’ll focus on object-oriented programming, where there’s more than one kind.

More than one kind

‘Polymorphism’ isn’t a single mechanism but a family of them. The classic map — from Cardelli and Wegner’s 1985 paper — splits it in two: universal polymorphism, which genuinely works across an open-ended range of types, and ad-hoc polymorphism, which only appears to.

  • Subtype A Shape reference holds any subtype. Universal; also called inclusion.
  • Parametric One class parameterised by type, like List<T>. Universal.
  • Overloading One name, several unrelated implementations. Ad-hoc.
  • Coercion Implicit conversion, e.g. int to double. Ad-hoc.

The three that earn their keep day to day are subtype polymorphism, overloading and generics — so those are the ones we’ll walk through.

Subtype polymorphism: references and overriding

As developers we’re interested in substitution (swapping one kind of object for another), the ability to plug in new components with new behaviour (creating new kinds of object) and to define general behaviour independently of the specific kind of object. That means we’re interested in having many forms of object; in computer science we use the specific term ‘type’. Objects themselves aren’t polymorphic — a given object only ever has one concrete type[1].

If objects don’t have many forms, do classes? They define many types, so you might think so, but a class is its types — all of its subclasses and their subclasses, and so on. We can’t talk about a class having many forms, as it is all of its forms.

You’ll often hear that objects are polymorphic — a handy shorthand, but strictly it’s the other way round. It’s the variable — the reference or pointer to the object — that is polymorphic. It can hold many forms of object, and the class of the reference defines the kinds of object it can hold. Relying on that substitution safely — every subtype honouring its supertype’s contract — is what the Liskov Substitution Principle formalises.

A single Shape variable pointing to a circle, a square or a triangle object Shape s
The reference holds the ‘many forms’: one Shape variable can point to a circle, a square or a triangle — whichever you assign.

Polymorphic references alone wouldn’t get us what we want: they let different kinds of object be substituted, but not their behaviour to change. Overriding is what lets a subclass replace a superclass’s methods with its own behaviour — without it, every subclass would behave the same. Overriding isn’t itself the polymorphism — the substitutability is; overriding is just what makes it so useful. (Tutorials often call overriding ‘runtime polymorphism’, which quietly conflates the two.)

In code, one Shape reference runs whichever draw() belongs to the object it actually points at — a choice made at run time, from the object’s real type:

class Shape  { void draw() {              } }
class Circle extends Shape { void draw() { /* a circle */ } }
class Square extends Shape { void draw() { /* a square */ } }

Shape s = new Circle();
s.draw();   // runs Circle.draw() — resolved at run time
A Shape superclass with Circle, Square and Triangle subclasses, each overriding draw() Shape draw() Circle draw() Square draw() Triangle draw()
A Shape reference can hold any subclass — each overrides draw() with its own behaviour.

Overloading

Methods can be polymorphic too. A class can have more than one form of a method; which one is invoked depends on the type of arguments supplied — worked out at compile time, from the static types the compiler can see:

int    area(int side)     { return side * side; }
int    area(int w, int h)  { return w * h; }
double area(Circle c)     { return Math.PI * c.r * c.r; }

area(4);      // area(int)     — chosen by the compiler
area(3, 5);   // area(int,int) — from the argument types
One method name, area, with three overloads chosen by their parameter types area(int side) area(int w, int h) area(Circle c) area() one name, chosen by argument types
One name, many forms — the compiler picks the right area from the argument types.
The crisp distinction

Overriding is resolved at run time, from the object’s actual type. Overloading is resolved at compile time, from the static types of the arguments. Same shape of idea — one name, many forms — bound at opposite ends of a program’s life.

Generics

Parametric polymorphism (generics in Java, templates in C++) gives a class many forms depending on its type parameter. Java’s Collection classes, for example, are defined independently of what they contain — so you can make a collection for a specific type and have the compiler enforce it. Without generics you’d either write a separate collection per type, or accept one that holds anything and lose that type safety.

List<String>  names  = new ArrayList<>();
List<Integer> counts = new ArrayList<>();

names.add("Ada");   // fine
names.add(42);      // won't compile — the type is checked for you
A generic class List of T takes the form List of String or List of Integer List<T> List<String> List<Integer>
One generic class, many forms — a new type for every type argument you supply.

Why it matters

Polymorphism is what lets a system grow without being rewritten. Code written against a Shape — or any abstraction — keeps working when you add a Hexagon years later: you plug in new behaviour instead of editing the code that already uses it. New types extend the system; they don’t disturb it.

That is the difference between software that ossifies and software that adapts — and it’s exactly what we mean when we talk about building polymorphic systems. If you have software that needs to bend to new requirements rather than break under them, let’s talk.

Common questions

What are the types of polymorphism?

The classic split, from Cardelli and Wegner, is universal versus ad-hoc. Universal covers subtype polymorphism (a Shape reference holding any subtype, via overriding) and parametric polymorphism (one generic class, like List<T>). Ad-hoc covers overloading (one name, several typed implementations) and coercion (implicit conversions).

What’s the difference between overriding and overloading?

Overriding is resolved at run time, from the object’s actual type — a Shape reference runs whichever draw() belongs to the object it points at. Overloading is resolved at compile time, from the static types of the arguments. Same idea, one name and many forms, bound at opposite ends of a program’s life.

Is overloading polymorphism?

Yes — it’s ad-hoc polymorphism: one method name with several unrelated implementations, chosen by the argument types. It’s a convenience rather than the open-ended substitutability that subtype and parametric polymorphism give you.

Is polymorphism the same as inheritance?

No. Inheritance is one common way to get subtype polymorphism — a subclass can stand in for its superclass — but the polymorphism is the substitutability itself, not the inheritance. You can also get it through interfaces or generics, without classical inheritance.


[1] An object of a specific type can belong to more than one class (interfaces, multiple inheritance); this is known as allomorphism.