OOP Fundamentals · Core OOP Concepts

Polymorphism, Interfaces & Abstract Classes

How dynamic dispatch replaces type checks, how overloading differs from overriding, and when to reach for an interface versus an abstract class.

  • 5 min read
  • 9 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The idea
  3. Overriding vs. overloading
  4. Interfaces vs. abstract classes
  5. How to apply it, step by step
  6. Where you'll see it
  7. Which tool for which job?
  8. Common mistakes
  9. Check yourself
  10. Key takeaways
  11. Quick revise
  12. Practice

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:

  1. The idea: each type overrides the behavior, and the right version runs at run time.
  2. Overriding versus overloading - similar names, very different rules.
  3. Interfaces versus abstract classes.
  4. How to refactor an if chain 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 def with the same name replaces the first. functools.singledispatchmethod dispatches 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

InterfaceAbstract class
A class can havemanyonly one
Fieldsconstants onlyany state
Methodsabstract, default, static, privateany, plus constructors
Best fora 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

  1. Spot a type check (instanceof, a type field, a switch on kind) that decides behavior.
  2. Move that behavior into a method on the common type (interface or abstract class).
  3. Let each type override it with its own version.
  4. Replace the if chain with a single call.
  5. 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 against List works with any list.
  • Comparable, Runnable, AutoCloseable - capabilities as interfaces, implemented by unrelated classes.
  • java.util.AbstractList - an abstract class that implements most of List so subclasses only write get and size.

Which tool for which job?

You want toUse
Replace an if/switch on a type fieldan overridden method on the common type
Describe a capability unrelated classes sharean interface (Java); an ABC or Protocol (Python)
Share code and state within one familyan abstract class that implements the interface
Accept any object that has the right methodsa Protocol (Python), an interface (Java)
Choose behavior by an argument's run-time type, outside the classesfunctools.singledispatch (Python); a pattern switch (Java 21)
Add many operations over a fixed set of typesa sealed interface plus an exhaustive switch

Common mistakes

MistakeWhat happensDo this instead
Expecting overloads to follow the object's real typeprint(Object) runs for a String held in an Object variableoverride, or dispatch on the run-time type
Two defs with the same name in a Python classthe second silently replaces the firstone method with defaults, or singledispatchmethod
A subclass that throws for an inherited methodcallers of the base type breakrethink the hierarchy (smaller interfaces)
Calling an overridable method from a constructorthe override sees unset fieldskeep constructors to assigning state
Declaring variables as the concrete classcode is tied to one implementationdeclare 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.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toUse
Replace a type-field if chainan overridden method
A capability for unrelated classesinterface (Java); ABC or Protocol (Python)
Shared code and state in a familyan abstract class implementing the interface
Dispatch on an argument's run-time typesingledispatch (Python); pattern switch (Java 21)
Many operations over a fixed set of typessealed interface + exhaustive switch

Snippets worth knowing by heart

Java

interface Employee { long monthlyPay(); }
record Salaried(long salary) implements Employee { public long monthlyPay() { return salary; } }
record Hourly(long hours, long rate) implements Employee { public long monthlyPay() { return hours * rate; } }
long payroll = employees.stream().mapToLong(Employee::monthlyPay).sum();

Python

class Employee(ABC):
    @abstractmethod
    def monthly_pay(self) -> int: ...

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)

The traps

  • Overloads follow the declared type, not the object.
  • Python has no overloading - a second def replaces the first.
  • Refused bequest - a subclass that throws for an inherited method.
  • Overridable calls in constructors - the override sees unset fields.

Staff-level points, one line each

  • Java dispatches instance methods only; fields and static methods are resolved by declared type (hiding).
  • Python attribute lookup is dynamic, so class attributes behave polymorphically.
  • Overload resolution: no boxing/varargs, then boxing, then varargs; most specific wins.
  • List<Integer>.remove(1) removes index 1; remove(Integer.valueOf(1)) removes the value.
  • singledispatch dispatches on the run-time type through the MRO.
  • Conflicting defaults: Java won't compile until you override (X.super.m()); Python's C3 MRO picks the first base.
  • super() follows the MRO; inconsistent bases fail at class creation.
  • Constructor-called overrides see null/0 in Java (constant finals are inlined) and AttributeError in Python.
  • The expression problem: polymorphism makes new types cheap; sealed types and switches make new operations cheap.
  • Interfaces are nominal in Java; Python Protocols are structural.

Say it in an interview

  • "Each employee type computes its own pay, so payroll is one call - no type checks."
  • "Types are added more often than operations here, so polymorphism beats a switch."
  • "I declare the parameter as the interface, so any implementation works."
  • "The constructor only assigns fields; the hook runs from start()."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Polymorphism, Interfaces & Abstract Classes free

Shapes: One Interface, One Abstract Base Class

MediumProCoding problemJava · Python

Shape is the interface every caller uses. Polygon is an abstract class that shares code between shapes with straight sides. Circle isn't a polygon, so it implements Shape directly. Fill in the blanks:

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Polymorphism, Interfaces & Abstract Classes free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime