This lesson was translated automatically – it may contain errors.
Polymorphism is a choice: which code exactly runs behind one and the same call. The question is when that choice is made. There are two answers, and they give two kinds of polymorphism.
Before taking those two apart, let us look at the stages a program goes through – from source text to a running process. The choice happens at those stages.
In the general case there are three stages:
- Program code – what the developer wrote: text containing type names, method signatures and calls. Nothing runs at this stage, but all the type information is there to see.
- Compilation – the compiler parses the text, checks the types and turns it into executable code. Everything that can be decided in advance is decided here: which overload fits, what a template expands into. Python and JavaScript have no separate stage – they go straight to execution and parse the code as they go.
- Execution – the program is running and the variables hold concrete objects. Only here does it become known what actually arrived in a method, and only here can an implementation be chosen by the actual type.
Next come the two kinds of polymorphism, one for each of the last two stages.
Static polymorphism
The choice is made before the program starts – by the compiler, from the types it sees in the code. Hence the other name: early binding.
This covers method and operator overloading, and generics too: C++ templates, C# and Java
generics, Box[T] in Go.
void Print(int value) => Console.WriteLine($"number {value}");
void Print(string value) => Console.WriteLine($"string {value}");
Print(42); // the compiler has already picked the first version
Print("hello"); // and the second one here
// The template is expanded for every type used, at build time:
// two different functions end up in the machine code
template <typename T>
void Print(const T& value) { std::cout << value << "\n"; }
Print(42);
Print(std::string("hello"));
What it gives you:
- speed – the finished program contains a direct call, there is nothing to resolve at run time;
- checks up front – a type mismatch fails the build instead of failing at the user;
- editor support – the IDE knows exactly which method will be called.
Break the rules and the program will not build
Since the compiler makes the choice, it also checks that there is something to choose from. Call Print(3.14) when the overloads are declared only for int and string, and the build fails with «no overload takes a double». Same with a template: Print(myOrder) will not compile if Order has no output operator.
That is a plus, not a minus: the mistake is found on your machine in seconds instead of at the user in production. Dynamic polymorphism cannot do that – there a missing method shows up only at the moment of the call.
What we pay: the choice is fixed at build time. Swapping an implementation on the fly – slipping a test stub in place of the real payment gateway, say – is no longer possible.
And the second price is build time. All the work that will not happen at run time does not disappear, it simply moves into the compiler: sift through overloads, infer types, expand a template for every type used. On large C++ projects that turns into minutes of waiting after every edit, and template error messages grow to dozens of lines. A fast program and a fast build are different things, and static polymorphism picks the first.
Dynamic polymorphism
The choice is made at run time – by the actual type of the object sitting in the variable right now. Hence the name late binding.
This covers virtual methods, interfaces and duck typing.
Notifier notifier = new EmailNotifier();
// The compiler only knows the type of the variable – Notifier.
// Which Send runs is decided by the runtime, from the actual object
notifier.Send("hi");
def speak(animal):
# No types at all: the method is looked up on the object at call time
animal.say()
speak(Dog())
speak(Cat())
What it gives you:
- flexibility – an implementation can be swapped without touching the calling code;
- extensibility – a new class is added without editing the old ones;
- testability – the real service is replaced by a stub in one line.
Break the rules and you find out at run time
The compiler can check nothing here: it sees the type Notifier, and which object ends up in the variable is settled at launch. Pass an object without a say method into speak – in Python that is an AttributeError at the moment of the call; in C# a null reference gives NullReferenceException and a failed cast gives InvalidCastException.
All of that happens at the user, and the build said nothing. So where static polymorphism gets by with a compiler, dynamic polymorphism needs tests.
What we pay: the call costs more – the runtime has to work out whose method to call (in C++, C# and Java that is the virtual method table) – and mistakes surface later, while the program is running.
Which to choose
Both kinds solve the same problem, but distribute the work and the risks differently:
| Static | Dynamic | |
|---|---|---|
| Build speed | lower: sifting overloads and expanding templates falls on the compiler | higher: there is almost nothing to resolve at build time |
| Run speed | higher: the finished program contains a direct call | lower: the implementation has to be found on every call |
| Finding mistakes | at build time, before launch | at run time, at the user |
| When the implementation is chosen | by the compiler, from types in the code | by the runtime, from the object in the variable |
| How it is written | overloading, templates, generics | virtual methods, interfaces, duck typing |
A couple of practical consequences.
Languages without a separate compilation step – Python, JavaScript – are dynamic by construction: almost every call there is resolved at the moment of execution. Strictly compiled languages offer both kinds and let you choose.
And the main point: static and dynamic is a classification, not the mechanisms themselves. It is how we divide polymorphism by when the choice is made, while the writing down is done by concrete language constructs – and a single language usually has several at once: C# has overloading, generics, virtual methods and interfaces.
Every one of those constructs – what it is, which languages have it and how it looks – is taken apart in the next lesson.