Skip to main content

Polymorphism πŸ“„

Polymorphism means:

β€œMany forms” β€” the same interface, different behavior.

But there are three fundamentally different kinds:

  1. Subtyping polymorphism (inheritance-based)
  2. Parametric polymorphism (generics)
  3. Ad-hoc polymorphism (overloading / operator overloading)

And we’ll connect each one to:

  • Encapsulation
  • Invariants
  • Contracts

Subtyping Polymorphism​

Also called: inclusion polymorphism

This is classic OOP polymorphism.

A subtype can be used wherever its supertype is expected.

Core Idea​

Shape s = new Circle(5);

A Circle is a Shape.

The behavior depends on the runtime type.

Example​

abstract class Shape {
public abstract double getArea();
}

Subtypes:

class Circle extends Shape {
private double radius;

public Circle(double radius) {
if (radius <= 0)
throw new IllegalArgumentException();
this.radius = radius;
}

public double getArea() {
return Math.PI * radius * radius;
}
}

class Rectangle extends Shape {
private double width, height;

public Rectangle(double width, double height) {
if (width <= 0 || height <= 0)
throw new IllegalArgumentException();
this.width = width;
this.height = height;
}

public double getArea() {
return width * height;
}
}

Usage:

Shape s1 = new Circle(5);
Shape s2 = new Rectangle(4, 6);

System.out.println(s1.getArea());
System.out.println(s2.getArea());

Same method call β†’ different behavior.

That’s polymorphism.

Connection to Encapsulation​

Each subclass:

  • Encapsulates its own data (radius, width, height)
  • Protects its own invariants
  • Exposes only getArea()

The abstraction (Shape) defines a contract:

Every shape must compute area.

Encapsulation ensures:

  • Each implementation maintains its internal correctness.

Subtyping only works safely if invariants are preserved (LSP).

If a subclass weakens the contract β†’ polymorphism becomes unsafe.

Parametric Polymorphism​

Also called:

  • Generics
  • Templates
  • Universal polymorphism

This means:

Code works uniformly for any type.

Example in Java (Generics)​

class Box<T> {
private T value;

public Box(T value) {
this.value = value;
}

public T getValue() {
return value;
}
}

Usage:

Box<Integer> intBox = new Box<>(10);
Box<String> strBox = new Box<>("Hello");

Same code.
Different types.

No inheritance involved.

What’s happening?​

The type parameter T is abstract.

The code does not care whether:

  • T is Integer
  • T is String
  • T is User
  • T is anything

The behavior does not change.

This is uniform behavior across types.

How It Relates to Encapsulation

Encapsulation ensures:

  • The internal state (value) is protected.
  • Type safety is maintained.
  • The invariant β€œBox always contains a T” is preserved.

Parametric polymorphism:

  • Does NOT change behavior based on type.
  • It guarantees uniform treatment.

Example invariant:

If Box<T> stores a T, it will only ever return a T.

That invariant is enforced by the type system.

danger

Dynamic Method Dispatch​

  • When a Parent type reference points to a Child object, overridden methods in Child are called at runtime.
  • The compiler only knows the reference type (Parent in p1) and not the actual object (Child).
  • If you have a reference of type Child, you can access all methods of Child, including inherited and new methods.
abstract class Parent{
protected String name;
Parent(String name){
this.name = name;
}
Parent(){
this.name = "Member";
}
abstract public void show();
}
// interface Parent{
// public void show();
// }

// class Child implements Parent{
class Child extends Parent{
Child(){

}
Child(String name){
super(name);
}
public void show(){
System.out.println(name +" -> Hello Show!");
}

void display(){
System.out.println(name +" -> Hello Display!");
}
}


class Main {
public static void main(String[] args) {
System.out.println("Try programiz.pro");
Parent p1 = new Child("Masum");
p1.show();
// p1.display(); // causes

Child ch1 = new Child();
ch1.show();
ch1.display();

// Array of Parent references holding different Child objects
Parent[] parents = { new ChildA(), new ChildB(), new ChildA() };

// Loop through the array and call show() - runtime polymorphism
for (Parent p : parents) {
p.show(); // Calls the overridden method of the actual object
}
}
}

Output

Hello Show!
Hello Show!
Hello Display!
  • You can initiate constructor of abstract class, but can't initiate constructor of interface, interface doesn't have constructor.

Ad-hoc Polymorphism​

This means:

Same function name, different implementations depending on types.

Two common forms:

  • Method overloading
  • Operator overloading

Example: Method Overloading​

class Calculator {

public int add(int a, int b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}
}

Same method name add
Different parameter types
Different behavior

The compiler chooses the correct one at compile time.

Example: Operator Overloading (C++)​

class Vector {
public:
int x, y;

Vector(int x, int y) : x(x), y(y) {}

Vector operator+(const Vector& other) {
return Vector(x + other.x, y + other.y);
}
};

Now:

Vector v1(1,2);
Vector v2(3,4);
Vector v3 = v1 + v2;

+ behaves differently for:

  • integers
  • floating numbers
  • vectors

That’s ad-hoc polymorphism.

Relation to Encapsulation​

Encapsulation ensures:

  • Internal representation of Vector is hidden.
  • Addition preserves invariants (e.g., valid coordinates).

Ad-hoc polymorphism does NOT rely on subtype relationships. It relies on:

  • Multiple function definitions
  • Compiler resolution

Side-by-Side Comparison​

TypeMechanismBehavior Changes?When Resolved
SubtypingInheritance / InterfacesYes (runtime)Runtime
ParametricGenerics / TemplatesNo (uniform)Compile-time
Ad-hocOverloadingYesCompile-time

Deep Conceptual Difference​

Subtyping:

β€œYou can replace me with my subtype.”

Parametric:

β€œI work for any type.”

Ad-hoc:

β€œI behave differently depending on argument types.”

Strong Encapsulation Perspective​

Polymorphism relies on encapsulation to be safe.

Without encapsulation:

  • Subtypes could break invariants.
  • Generics could expose unsafe casts.
  • Overloading could rely on shared mutable state.

Encapsulation ensures:

  • Internal correctness.
  • Invariants preserved regardless of polymorphic behavior.

One Unified Example​

Imagine a system:

interface Printer {
void print();
}

Subtyping polymorphism:

class PDFPrinter implements Printer { ... }
class TextPrinter implements Printer { ... }

Parametric polymorphism:

class PrinterQueue<T extends Printer> { ... }

Ad-hoc polymorphism:

void print(String message)
void print(int number)

All three forms coexist in real systems.

Final Insight​

Polymorphism is about flexibility of behavior.

Encapsulation is about protection of correctness.

Abstraction defines contracts.
Inheritance enables subtyping.
Generics enable uniformity.
Overloading enables convenience.

But none of them are safe without invariants being protected.

The Most Important Takeaway​

Subtyping polymorphism is the most powerful β€”
and the most dangerous β€”
because it can violate invariants if misused.

Parametric polymorphism is the safest β€”
because behavior is uniform.

Ad-hoc polymorphism is the simplest β€”
but limited.