Polymorphism

On this page 7

Polymorphism in object-oriented programming is the ability of one interface or method call to work with objects of different types, with each object responding in a way that suits its own class. A caller can ask any shape to draw itself or any payment method to process a charge, without knowing which concrete class it is dealing with.

The word comes from Greek for many forms. Polymorphism is one of the core principles of object-oriented design, alongside encapsulation and inheritance, and it is the main reason object-oriented code can be extended with new types without changing existing callers.

How polymorphism works at run time

Run-time polymorphism works through dynamic dispatch: the program decides which method implementation to execute based on the actual class of the object, not on the declared type of the variable that refers to it. A variable of type Shape may hold a Circle or a Rectangle, and calling draw runs the version defined by that specific object.

Most compiled object-oriented languages implement this with a virtual method table (vtable). Each class has a table of pointers to its method implementations, and each object holds a hidden pointer to its class table. A polymorphic call loads that pointer, looks up the method slot and jumps to it, which adds one or two memory indirections compared with a direct call.

In C++, a method takes part in dynamic dispatch only when declared virtual. Instance methods are virtual by default in Java but opt-in through the virtual keyword in C#, while dynamically typed languages such as Python resolve every method call at run time by looking up the name on the object.

Kinds of polymorphism

Polymorphism is an umbrella term for several distinct mechanisms. Computer science usually distinguishes subtype, parametric and ad hoc polymorphism, and the distinction matters because each is resolved at a different time and suits different problems.

KindMechanismResolvedExample
Subtype polymorphismOverriding methods of a base class or interfaceAt run timeShape.draw on Circle or Square
Parametric polymorphismGeneric code over any typeUsually at compile timeList of T, generic functions
Ad hoc polymorphismOverloading by parameter typesAt compile timeprint for integers and strings
Duck typingAny object with the right methods is acceptedAt run timePython objects with a read method
Operator overloadingOperators defined per typeAt compile time or run timeAdding two vectors with +
Main kinds of polymorphism

Languages without classical inheritance still offer polymorphism. Go uses implicitly satisfied interfaces, Rust uses traits with either static or dynamic dispatch, and Haskell uses type classes.

Polymorphism compared with inheritance and interfaces

Polymorphism is often confused with inheritance, yet inheritance is only one way to achieve it. Inheritance reuses and extends code from a parent class, while polymorphism concerns treating different types uniformly through a shared contract.

ConceptMain purposeRelation to polymorphism
InheritanceReuse and extend a base classProvides subtype polymorphism through overriding
InterfaceDefine a contract without implementationCleanest basis for polymorphic calls
EncapsulationHide internal state behind methodsLets each type implement behavior privately
Abstract classPartial implementation with required methodsMix of shared code and polymorphic hooks
CompositionBuild objects from other objectsOften combined with interfaces instead of inheritance
Polymorphism and neighboring object-oriented concepts

Modern design guidance often recommends programming to an interface rather than an implementation and preferring composition over deep inheritance hierarchies, because interfaces give polymorphism without the tight coupling of a shared base class.

Using polymorphism in design

Polymorphism is most useful where code would otherwise branch on the type of an object. A long chain of if or switch statements that checks a type field is a classic signal that a polymorphic method should replace it.

SituationPolymorphic approachBenefit
Many types share an action but differ in detailsCommon interface with overridden methodsCallers stay unchanged
New variants are added frequentlyNew class implements the interfaceFollows the open-closed principle
Algorithm varies at run timeStrategy patternBehavior is swapped without conditionals
Tests need a replacement for a slow dependencyMock or fake implementing the same interfaceIsolated, fast unit tests
Set of types is fixed and smallPattern matching on a closed type may suit betterExhaustiveness checked by compiler
When to apply polymorphism

Polymorphism also supports the Liskov substitution principle: any object of a subtype must be usable wherever its base type is expected without breaking the caller’s assumptions.

Pitfalls and misconceptions

Polymorphism brings flexibility at the cost of indirection and less obvious control flow. Experienced developers watch for several recurring problems.

  • Violating substitutability: a subclass that throws on an inherited method or changes its meaning breaks every polymorphic caller.
  • Overloading mistaken for overriding: an overload is chosen at compile time from declared types, so it does not dispatch dynamically.
  • Missing virtual destructor in C++: deleting a derived object through a base pointer without a virtual destructor causes undefined behavior.
  • Object slicing: copying a derived object into a base-class value in C++ discards the derived part and its overrides.
  • Over-abstraction: interfaces with a single implementation and no expected variation add indirection without benefit.
  • Performance in hot loops: dynamic dispatch can prevent inlining, although the cost is negligible in most application code.

Explained without jargon

Polymorphism means one request, many correct responses. You give the same instruction to different things, and each one carries it out in its own way.

Think of saying “play” to a music player, a video player and a game console. Each one starts something different, yet you used the same word. You do not need to know how each device works inside. In software, this lets programmers add new kinds of objects without rewriting the code that uses them.

A typical use case

An online store’s checkout supports card payments and bank transfers. The original code contains a large switch statement that checks the payment type in five different places, and adding a digital wallet would require editing all of them.

The team defines a PaymentMethod interface with authorize, capture and refund methods. Each payment type becomes a class that implements the interface, and the checkout calls the methods without checking which class it holds. Adding the digital wallet now means writing one new class and registering it.

Unit tests use a fake payment class that implements the same interface, so checkout logic is tested in milliseconds without contacting a real payment provider. The result is fewer touched files per change and a measurable drop in payment-related defects after each release.