On this page
Description
Polymorphism lets one call mean different things depending on the object that receives it, so code can work with many kinds of objects without checking which kind each one is. Interfaces (and abstract classes) are how you name the shared shape those objects promise. This skill covers dynamic dispatch, the difference between overriding and overloading, and when to reach for an interface versus an abstract class - in Java and Python.
Here's the kind of code polymorphism replaces:
Java
double pay(Employee e) {
if (e.type.equals("SALARIED")) return e.monthlySalary;
if (e.type.equals("HOURLY")) return e.hours * e.rate;
if (e.type.equals("CONTRACTOR")) return e.invoiceTotal;
throw new IllegalArgumentException(e.type);
}
Python
def pay(e: Employee) -> int:
if e.type == "SALARIED":
return e.monthly_salary
if e.type == "HOURLY":
return e.hours * e.rate
if e.type == "CONTRACTOR":
return e.invoice_total
raise ValueError(e.type)
The same if chain appears for tax, for benefits, for payslips... Every new employee type edits all of them, and forgetting one is a silent bug.
We'll go from simple to more technical:
- The idea: each type overrides the behavior, and the right version runs at run time.
- Overriding versus overloading - similar names, very different rules.
- Interfaces versus abstract classes.
- How to refactor an
ifchain into polymorphism, step by step.
The idea
Polymorphism lets the object decide what a method call means. Each type overrides the method, and Java picks the right version at runtime based on the object's actual class - this is called dynamic dispatch.
Java
abstract class Employee {
abstract long monthlyPay();
}
class Salaried extends Employee {
private final long salary;
Salaried(long salary) { this.salary = salary; }
long monthlyPay() { return salary; }
}
class Hourly extends Employee {
private final long hours, rate;
Hourly(long hours, long rate) { this.hours = hours; this.rate = rate; }
long monthlyPay() { return hours * rate; }
}
long payroll = employees.stream().mapToLong(Employee::monthlyPay).sum(); // no if-chain
Python
from abc import ABC, abstractmethod
class Employee(ABC):
@abstractmethod
def monthly_pay(self) -> int: ...
class Salaried(Employee):
def __init__(self, salary: int):
self._salary = salary
def monthly_pay(self) -> int:
return self._salary
class Hourly(Employee):
def __init__(self, hours: int, rate: int):
self._hours, self._rate = hours, rate
def monthly_pay(self) -> int:
return self._hours * self._rate
payroll = sum(e.monthly_pay() for e in employees) # no if-chain
Overriding vs. overloading
These sound alike but work completely differently:
- Overriding - a subclass redefines an inherited method with the same signature. The choice is made at runtime, based on the object's real class.
- Overloading - several methods share a name but take different parameter types. The choice is made at compile time, based on the variable's declared type. (Python has no overloading: a second
defwith the same name replaces the first.functools.singledispatchmethoddispatches on the argument's runtime type instead.)
Java
class Printer {
void print(Object o) { System.out.println("object"); }
void print(String s) { System.out.println("string"); }
}
Object x = "hello";
new Printer().print(x); // prints "object" - overloads are chosen by the DECLARED type
Python
class Printer:
def show(self, o: object) -> None:
print("object")
def show(self, s: str) -> None: # not an overload: this REPLACES the method above
print("string")
x: object = "hello"
Printer().show(x) # prints "string" - Python keeps only the last definition
Interfaces vs. abstract classes
| Interface | Abstract class | |
|---|---|---|
| A class can have | many | only one |
| Fields | constants only | any state |
| Methods | abstract, default, static, private | any, plus constructors |
| Best for | a capability many unrelated types share (Comparable, Runnable) | a family that shares code and state |
Rule of thumb: define the type as an interface; add an abstract class only if several implementations genuinely share code - and let it implement the interface.
In Python both are classes: an "interface" is an abstract base class with only abstract methods, or a typing.Protocol, which any class with the right methods satisfies without inheriting from it. A class can inherit from several.
How to apply it, step by step
- Spot a type check (
instanceof, atypefield, aswitchon kind) that decides behavior. - Move that behavior into a method on the common type (interface or abstract class).
- Let each type override it with its own version.
- Replace the
ifchain with a single call. - Program to the interface: declare variables and parameters as
Shape,List,Employee- not the concrete class.
Where you'll see it
List<String> names = new ArrayList<>();- code written againstListworks with any list.Comparable,Runnable,AutoCloseable- capabilities as interfaces, implemented by unrelated classes.java.util.AbstractList- an abstract class that implements most ofListso subclasses only writegetandsize.
Which tool for which job?
| You want to | Use |
|---|---|
Replace an if/switch on a type field | an overridden method on the common type |
| Describe a capability unrelated classes share | an interface (Java); an ABC or Protocol (Python) |
| Share code and state within one family | an abstract class that implements the interface |
| Accept any object that has the right methods | a Protocol (Python), an interface (Java) |
| Choose behavior by an argument's run-time type, outside the classes | functools.singledispatch (Python); a pattern switch (Java 21) |
| Add many operations over a fixed set of types | a sealed interface plus an exhaustive switch |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Expecting overloads to follow the object's real type | print(Object) runs for a String held in an Object variable | override, or dispatch on the run-time type |
Two defs with the same name in a Python class | the second silently replaces the first | one method with defaults, or singledispatchmethod |
| A subclass that throws for an inherited method | callers of the base type break | rethink the hierarchy (smaller interfaces) |
| Calling an overridable method from a constructor | the override sees unset fields | keep constructors to assigning state |
| Declaring variables as the concrete class | code is tied to one implementation | declare the interface (List, Shape) |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. Object x = "hi"; printer.print(x); - with overloads print(Object) and print(String), which runs?
Show answer
print(Object). Overloads are resolved at compile time using the declared type of x, which is Object.
2. What decides which overridden method runs: the variable's type or the object's type?
Show answer
The object's actual class, at runtime (dynamic dispatch).
3. You need a Comparable-like capability for many unrelated classes. Interface or abstract class?
Show answer
An interface - a class can implement many interfaces but extend only one class, and capabilities don't need shared state.
Key takeaways
- Polymorphism replaces type checks with one call; each class overrides the behavior.
- Overriding is chosen at runtime by the object's class; overloading at compile time by the declared type.
- Interfaces describe capabilities (many per class, no state); abstract classes share code and state (one per class).
- Program to interfaces, not implementations.