Polymorphism π
Polymorphism means:
βMany formsβ β the same interface, different behavior.
But there are three fundamentally different kinds:
- Subtyping polymorphism (inheritance-based)
- Parametric polymorphism (generics)
- 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 aT, it will only ever return aT.
That invariant is enforced by the type system.
Dynamic Method Dispatchβ
- When a Parent type reference points to a Child object, overridden methods in
Childare called at runtime. - The compiler only knows the reference type (
Parentinp1) 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β
| Type | Mechanism | Behavior Changes? | When Resolved |
|---|---|---|---|
| Subtyping | Inheritance / Interfaces | Yes (runtime) | Runtime |
| Parametric | Generics / Templates | No (uniform) | Compile-time |
| Ad-hoc | Overloading | Yes | Compile-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.