Explainer · Programming

What Is Polymorphism in Programming?

Overriding, overloading and generics, with examples in Java.

In programming, polymorphism is one piece of code working with values of many forms, or types. The code is written once, against a general type such as Shape, and works on circles, squares and shapes nobody had thought of when it was written.

It is also the idea behind our name: most of our work is on systems that have to keep taking on new requirements without a rewrite.

The polymorphism quiz How well do you know polymorphism? Thirty questions in three rounds, from beginner to expert, mostly in Java, with Python, TypeScript and others. Take the quiz

1 The idea in one example

Here is a method that draws a list of shapes. It is written against Shape, so it never knows which kind of shape it is holding. (The examples are Java 25, which lets a short program print with a plain IO.println. The ideas are the same in any language that has them.)

abstract class Shape {
    abstract void draw();
}

class Circle extends Shape {
    void draw() { IO.println("a circle"); }
}
class Square extends Shape {
    void draw() { IO.println("a square"); }
}

void drawAll(List<Shape> shapes) {
    for (Shape s : shapes) s.draw();
}

Later, someone needs hexagons. They write one new class and use it:

class Hexagon extends Shape {
    void draw() { IO.println("a hexagon"); }
}

drawAll(List.of(new Circle(),
                new Hexagon()));
// a circle
// a hexagon

drawAll was not edited. Each object brings its own draw(), and the call s.draw() runs whichever one belongs to the object in hand. This is what lets a system grow without being rewritten. The trade is that a new operation, such as area(), now has to be written in every class, so a good design decides early whether it expects more new types or more new operations.

Check yourself You add an abstract method, perimeter(), to the abstract class Shape. What has to change?
  1. Nothing: every subclass inherits it
  2. Every concrete subclass must implement it
  3. Only drawAll, the code that calls it
Show answer

Every concrete subclass must implement it. Adding a type is cheap: one class, and nothing else touched. Adding an operation is expensive: every class needs one. (Had perimeter() been given a body in Shape, subclasses would inherit it.)

If you have a system that has to keep growing without a rewrite, let’s talk.

2 Overriding vs overloading

Compile time is when the compiler checks and translates your code, before it runs; run time is when it runs, with real objects.

In one line

Overriding picks a method from the object’s actual class, at run time. Overloading picks one from the arguments’ declared types, at compile time. One name and many forms, bound at opposite ends of a program’s life.

Python and JavaScript do what drawAll does without writing any types down: call s.draw() and whatever object s holds answers, which is called duck typing. Java, C# and TypeScript let you declare what a variable can hold, as in Shape s, and the compiler checks every use against that declaration. That declared type is what the distinction turns on.

The difference shows in a well-known trap:

double area(Shape s)  { … }
double area(Circle c) { … }

Shape s = new Circle();
s.draw();  // Circle.draw(): run time
area(s);   // area(Shape): compile time

The compiler can see new Circle() on the line above, but Java’s rule is to choose an overload from the declared type alone. You’ll hear the two called dynamic and static polymorphism, or runtime and compile-time polymorphism. The rule holds in Java, C++ and C# (short of C#’s dynamic); a few languages, such as Julia, choose overloads at run time from the arguments’ actual types.

3 The kinds of polymorphism

‘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 works across an open-ended range of types, and ad-hoc polymorphism, which works for a fixed list of them.

  • Subtype A Shape variable holds any subtype. Universal; also called inclusion.
  • Parametric Code that works the same for any type. Generics, like List<T>, are its usual form. Universal.
  • Overloading One name, several unrelated implementations. Ad-hoc.
  • Coercion Implicit conversion, e.g. int to double. Ad-hoc.

The names on the ad-hoc side aren’t the ones you’ll usually hear. When people say ‘ad-hoc polymorphism’ they almost always mean overloading, and coercion is normally just called implicit conversion or type promotion: nobody debugging 1 + 2.5 thinks of it as polymorphism, and it’s on the map for completeness. The split itself is older than the paper. Christopher Strachey drew the line between ad-hoc and parametric polymorphism in 1967, and Cardelli and Wegner refined it into these four.

The three you’ll meet day to day are subtype polymorphism, overloading and parametric polymorphism.

Check yourself Math.sqrt(4) returns 2.0. Which kind of polymorphism lets it accept an int?
IO.println(Math.sqrt(4)); // 2.0
  1. Overloading
  2. Subtype
  3. Coercion
Show answer

Coercion. There is only one Math.sqrt, and it takes a double. Java converts the int to 4.0 before the call, so the method appears to work on both. That implicit conversion is coercion.

4 Subtype polymorphism and overriding

A Circle object has one class, Circle, but more than one type: it is a Circle, a Shape and an Object all at once. That is why a Shape variable can hold it, and why code written against Shape accepts it. Relying on that substitution safely, with 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
One Shape variable can point to a circle, a square or a triangle, whichever you assign.

Substitution alone would let different kinds of object stand in for each other, but they would all behave alike. Overriding is what lets a subclass replace a superclass’s method with its own, so that s.draw() does something different for each.

A Shape superclass with Circle, Square and Triangle subclasses, each overriding draw() Shape draw() Circle draw() Square draw() Triangle draw()
Each subclass overrides draw() with its own behaviour.
Check yourself What happens on the second line?
Shape s = new Circle();
Circle c = s;
  1. It compiles, and c holds the circle
  2. It doesn’t compile without a cast
  3. It compiles, then throws at run time
Show answer

It doesn’t compile without a cast. Every Circle is a Shape, but not every Shape is a Circle. The compiler goes by the declared type, so going down needs an explicit cast: (Circle) s.

5 Overloading

A class can also have several methods with the same name, told apart by their parameters. The compiler picks one from the types of the arguments:

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

area(4);      // area(int)
area(3, 5);   // area(int, int)
area(2.5);    // area(double)
One method name, area, with three overloads chosen by their parameter types area(int side) area(int w, int h) area(double r) area() one name, chosen by argument types
One name, many forms: the compiler picks the right area from the argument types.
Check yourself Do these two methods compile side by side?
int    parse(String s) { … }
double parse(String s) { … }
  1. Yes: the return types differ
  2. Yes, if the class is abstract
  3. No: the parameters are the same
Show answer

No: the parameters are the same. Java tells overloads apart by their parameters alone. A call like parse("4") could mean either, so the second is rejected as already defined.

6 Parametric polymorphism: generics

Parametric polymorphism is code written once that works the same for any type. Generics are how Java, C# and TypeScript provide it; C++ templates reach a similar result by generating code for each type. Java’s collections are defined independently of what they contain, so you can make a list of one specific type and have the compiler enforce it. Without generics you’d either write a separate list 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
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 types to the compiler. Java erases the difference at run time, so both are one ArrayList class underneath.
Check yourself What does this print?
var a = new ArrayList<String>();
var b = new ArrayList<Integer>();
boolean same =
    a.getClass() == b.getClass();
IO.println(same);
  1. false
  2. true
  3. It doesn’t compile
Show answer

true. Java erases type arguments when it compiles, so at run time both are the same ArrayList class. The difference exists only for the compiler.

7 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 variable holding any subtype) and parametric polymorphism (which Java and C# provide as generics, like List<T>). Ad-hoc covers overloading (one name, several typed implementations) and coercion (implicit conversions).

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, because a subclass can stand in for its superclass. Interfaces give you the same substitutability without classical inheritance, and generics give you a different kind altogether: parametric polymorphism.

Does JavaScript or TypeScript have overloading?

JavaScript doesn’t: a second function with the same name replaces the first, so one function checks its arguments instead. TypeScript lets you declare several overload signatures, but they share one implementation and disappear when the code is compiled, so the one implementation still checks its arguments at run time.

Is a List<Circle> a List<Shape>?

Not in Java. If it were, you could add a Square to a list of circles through it. Write List<? extends Shape> for a list you only read shapes from. Arrays are covariant, so Shape[] a = new Circle[1] compiles, and storing a Square in it throws ArrayStoreException at run time.

Polymorphism is also the name of a small senior team that builds and runs large systems for the public sector, enterprises and growing businesses, whose systems have to keep taking on new requirements. Modernising legacy systems without a rewrite is how that is done on a real one.

Test yourself How much of it stuck? Thirty questions in three rounds, from beginner to expert, and a link at the end to try a colleague on the same ones. Take the quiz