Polymorphism
One call, many behaviors: code written against a common type runs the right method for each object at run time. It is how designs grow without a switch statement in the middle.
Overview
Polymorphism means that one piece of code can work with values of many types. In object-oriented design the form that matters is subtype polymorphism: you write shape.area() once, against the Shape type, and the runtime runs Circle.area or Rectangle.area depending on which object is really there. The caller never asks what kind of shape it has.
That is the mechanism behind almost every design pattern and behind the Open/Closed Principle: to add a new shape you add a new class, and no existing code changes. Interviewers listen for whether you reach for it instead of a chain of type checks, and whether you can say what happens underneath when the call is made.
The kinds of polymorphism
Luca Cardelli and Peter Wegner's 1985 survey sorted polymorphism into two families. Universal polymorphism is one piece of code that works for an unbounded set of types. Ad hoc polymorphism is a set of separate pieces of code that happen to share a name. Interviewers say 'polymorphism' and mean inclusion (subtype) polymorphism unless they say otherwise; the table gives you the words for the other three.
- Inclusion (subtype) — universal — a value of a subtype can be used where the supertype is expected — Shape s = new Circle(1)
- Parametric — universal — one definition with a type parameter — List, def first(xs: list[T]) -> T
- Overloading — ad hoc — several methods share one name; the compiler picks by argument types — print(int), print(String)
- Coercion — ad hoc — an argument is converted to the type a method expects — int passed where double is needed
The two universal kinds are the ones that make designs extensible. Overloading is a naming convenience resolved before the program runs.
What happens when the call is made
For a subtype-polymorphic call the compiler checks that the declared type has the method. The runtime then finds the method to run by looking at the actual object.
- Java — every instance method; static and private methods do not, and final cannot be overridden — JLS §15.12.4.4: the run-time class of the target reference is used
- C++ — only methods declared virtual in the base; others bind by the pointer's static type — the virtual specifier; override and final since C++11
- Python — every method; lookup walks the object's class and its MRO — the data model and the C3 MRO
- Go — every method of an interface value; there are no classes to override — interface values carry the concrete type
Compilers usually implement this with a per-class table of method addresses, so a virtual call is one extra indirection compared with a direct call. That cost is real but tiny, and it is not a reason to avoid polymorphism in application code.
Behavior in the class, not in a switch
The design payoff is removing conditionals on type. A switch over 'what kind is this' has to appear in every operation, and every new kind means editing every one of them. With polymorphism the knowledge of each kind lives in one class.
- Adding a new kind — edit every switch in the codebase — add one class
- Adding a new operation — add one switch — edit every class
- Where a kind's logic lives — scattered — in its class
- Compiler help — only with sealed types and exhaustive switches — missing override is a compile error (Java) or an instantiation error (Python ABC)
The second row is the honest trade-off: polymorphism makes adding kinds cheap and adding operations expensive. When the set of kinds is closed and operations change often, a sealed type with an exhaustive switch is the better tool.
What you will be able to do
- Explain subtype polymorphism with a diagram and say what is decided at compile time and what at run time.
- Name the four kinds from Cardelli and Wegner and say which one an interviewer means.
- Describe dynamic dispatch in Java, C++ and Python without hand-waving.
- Refactor a switch on a type field into classes, and say when you would not.
- Show polymorphism without inheritance: duck typing, Protocols, Go interfaces and generics.
Interview questions
- What is the difference between compile-time and run-time polymorphism?
- How would you refactor a switch on a type field into polymorphism?
- How does dynamic dispatch work under the hood?
- Can you have polymorphism without inheritance?
- When is a switch better than polymorphism?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio