4 The 4th & Final Pillar of OOP  ·  Chapter 11

🔄 Polymorphism

Many Forms  ·  One Power

Hello, my students! Today we arrive at the last and fourth pillar of Object-Oriented Programming. It is a fascinating one — but remember: polymorphism cannot be understood without inheritance. No inheritance = no polymorphism! Make sure Module 6 is solid before continuing.
✓ Encapsulation
✓ Abstraction
✓ Inheritance
► Polymorphism

1 What Does "Polymorphism" Mean?

Before we write a single line of code, let's understand the word itself. Polymorphism is not an English word — it comes from ancient Greek!

poly
Greek: πολύς
many
+
morphe
Greek: μορφή
form / shape
=
Polymorphism
Many Forms
Many Types  ·  Many Shapes
📚 Definition
Polymorphism is the ability to treat objects of different types as objects of a single common type — provided they are related through inheritance. In Java, this means one variable, one array, or one method can work uniformly with many different object types.

2 Software Is a Universe of Objects

Let us step back and revisit the fundamental definition of software:

🌐 The Definition of Software
Software is a collection of objects that interact with each other.

Think about our real universe. We have humans, lions, wolves, birds, fish, monkeys — thousands and thousands of different creatures. They are all objects. Now imagine building a software system that models a zoo, a wildlife simulation, or a biology database — you would have an enormous number of different object types!

👦
Human
🦁
Lion
🐺
Wolf
🐦
Bird
🐨
Monkey
🐡
Fish

How can a system handle thousands of different types without becoming unmanageable? The answer was figured out centuries ago — not by programmers, but by biologists.

3 The Biology Lesson — The Power of Categorization

When biologists study living creatures, they don't study every single species one by one. Instead, they group (categorize) them based on shared characteristics. This is called taxonomy or biological classification.

🧬 Biological Classification Examples
🦈
Vertebrates
فقاريات
Have a backbone
🐛
Invertebrates
لافقاريات
No backbone
🐶
Mammals
ثدييات
Warm-blooded, nurse young
🐸
Reptiles
زواحف
Cold-blooded, scaled

But why do biologists categorize? There are excellent reasons — and they are exactly the same reasons we use polymorphism in software:

📋
Shared Behaviors
All members of a group share similar properties and behaviors — study them together!
💊
One Medicine
Design one treatment for the entire group — not one per species!
📈
Scalability
Instead of 10,000 species individually — study the groups. Much more manageable!
💡 The Connection to OOP
This is exactly the same idea in software! Instead of writing separate code for every specific object type, we group them by their common superclass and write code that works with the entire group at once. This is polymorphism.

4 The Problem — Life Without Polymorphism

Imagine we are building a drawing application with 5 types of shapes:

Circle
Circle[]
Rectangle
Rectangle[]
Triangle
Triangle[]
Square
Square[]
Pentagon
Pentagon[]

Without polymorphism, to store 3 objects of each type and display them all, here is what we need:

🚫 Without Polymorphism — The Nightmare
For 5 shape types: 5 separate arrays, 5 separate loops, and 5 separate methods. And for 100 shape types? 100 of each!
📄 Without polymorphism — lots of repetition
// ❌ 5 DIFFERENT ARRAYS — one for each type! Circle[] circles = new Circle[3]; Rectangle[] rectangles = new Rectangle[3]; Triangle[] triangles = new Triangle[3]; Square[] squares = new Square[3]; Pentagon[] pentagons = new Pentagon[3]; // ❌ 5 DIFFERENT LOOPS — one for each type! for (int i = 0; i < circles.length; i++) { display(circles[i]); } for (int i = 0; i < rectangles.length; i++) { display(rectangles[i]); } for (int i = 0; i < triangles.length; i++) { display(triangles[i]); } for (int i = 0; i < squares.length; i++) { display(squares[i]); } for (int i = 0; i < pentagons.length; i++) { display(pentagons[i]); } // ❌ 5 DIFFERENT METHODS — one for each type! static void display(Circle c) { System.out.println(c.getArea()); } static void display(Rectangle r) { System.out.println(r.getArea()); } static void display(Triangle t) { System.out.println(t.getArea()); } static void display(Square s) { System.out.println(s.getArea()); } static void display(Pentagon p) { System.out.println(p.getArea()); }
⚠ The Challenge
What if we had 100 different shapes? We would need 100 arrays, 100 loops, and 100 methods — for every single operation! This violates the DRY principle (Don't Repeat Yourself) massively. We need a better way.

5 The Solution — Polymorphism in Action

Just like biologists group creatures under a common category (e.g., mammals), we can group all our shapes under their common superclass: Shape. Now we need only ONE of everything!

classDiagram direction TB class Shape { -String color +getColor() String +getArea() double +toString() String } class Circle { -double radius +getArea() double +getDiameter() double } class Rectangle { -double width -double height +getArea() double } class Triangle { -double base -double height +getArea() double } Shape <|-- Circle : extends Shape <|-- Rectangle : extends Shape <|-- Triangle : extends

Now look at the power of polymorphism — we write the solution once:

✓ With Polymorphism — ONE of Everything!
One array  ·  One loop  ·  One method
📄 With polymorphism — clean and scalable
// ✅ ONE POLYMORPHIC ARRAY — holds any Shape subclass! Shape[] shapes = new Shape[6]; shapes[0] = new Circle("Red", 5.0); shapes[1] = new Rectangle("Blue", 4.0, 6.0); shapes[2] = new Triangle("Green", 3.0, 4.0); shapes[3] = new Circle("Pink", 2.5); shapes[4] = new Rectangle("Gold", 8.0, 2.0); shapes[5] = new Triangle("Purple",6.0, 3.0); // ✅ ONE POLYMORPHIC LOOP — works for ALL shape types! for (int i = 0; i < shapes.length; i++) { display(shapes[i]); // same call — different shape each time } // ✅ ONE POLYMORPHIC METHOD — one parameter, many types! static void display(Shape s) { // ← polymorphic parameter System.out.println("[" + s.getColor() + "] Area = " + s.getArea()); }
Output
[Red] Area = 78.54 [Blue] Area = 24.00 [Green] Area = 6.00 [Pink] Area = 19.63 [Gold] Area = 16.00 [Purple] Area = 9.00

Types The 3 Forms of Polymorphism in Java

🔄
Polymorphic Variable
Shape s = new Circle(5);
A superclass variable holding a subclass object
📌
Polymorphic Array
Shape[] s = new Shape[10];
An array declared with the superclass type
📄
Polymorphic Method
void show(Shape s) {...}
A method with a superclass type parameter

6 The Golden Condition for Polymorphism

Polymorphism does not work with just any combination of objects. There is one non-negotiable condition:

🔑 The Golden Rule
To treat objects of different types as the same type,
they MUST be related through Inheritance (the IS-A relationship).
✓ Valid — IS-A Relationship Exists
Shape s = new Circle();
// Circle IS-A Shape ✓
Shape s = new Rectangle();
// Rectangle IS-A Shape ✓
Shape s = new Triangle();
// Triangle IS-A Shape ✓
✗ Invalid — No Inheritance Relationship
Shape s = new Student();
// Student is NOT a Shape ✗
Shape s = new Car();
// Car is NOT a Shape ✗
Shape s = new Book();
// Book is NOT a Shape ✗
💡 Why the IS-A Relationship?
If Circle extends Shape, then every Circle object is also a Shape object — it inherits all of Shape's behaviors. This is why we can safely treat a Circle as a Shape. A Student doesn't inherit from Shape, so it doesn't share Shape's behaviors — treating it as a Shape would make no sense.

7 What Can a Polymorphic Reference See?

This is crucial. When you hold an object through a polymorphic reference (like Shape s), you can only see the methods defined in the superclass. The subclass-specific methods are hidden.

🔍 What does this reference see?
Shape s = new Circle(5.0);
✓ getArea() ✓ getColor() ✓ toString()
Accessible — defined in Shape (superclass)
Inaccessible — defined only in Circle (subclass)
📄 Polymorphic scope example
Shape s = new Circle(5.0); s.getArea(); // ✅ OK — getArea() is declared in Shape s.getColor(); // ✅ OK — getColor() is declared in Shape s.toString(); // ✅ OK — toString() is declared in Shape s.getDiameter(); // ❌ COMPILE ERROR! getDiameter() is only in Circle
💡 Think of it as a Filter
A polymorphic reference acts like a "Shape filter". When you look through a Shape filter, you can only see what every Shape knows how to do — the shared, general behaviors. The specific Circle abilities are hidden behind the filter.

This is the price of generality: you gain flexibility (one reference for many types), but you lose direct access to subclass-specific methods. To regain access, you need casting — which we cover next.

8 The Coffee Cup Analogy

Let's make these concepts completely clear with a real-life analogy that everyone can relate to!

Universal Coffee Cup
🔄 The Polymorphic Cup
A general cup — can hold any hot drink!
Cappuccino Latte Tea Hot Choc. Arabic Coffee
Many types ✓
Arabian Coffee Cup
🚫 The Non-Polymorphic Cup
The Arabic coffee cup (فنجان) — only for Arabic coffee!
Arabic Coffee only
Cannot hold cappuccino, latte, or tea — wrong cup!
💡 Making the Connection to Code
  • General Cup = Polymorphic variable HotDrink cup — can hold any subtype
  • Arabic Coffee Cup = Non-polymorphic variable ArabianCoffee cup — holds one specific type only
  • The general cup is more flexible (polymorphic) — works with many drinks
  • The Arabic coffee cup is more specific (non-polymorphic) — works with one drink
📄 The cup analogy in Java code
// ☕ Polymorphic cup — any HotDrink works! HotDrink cup; // ← polymorphic variable (the general cup) cup = new Cappuccino(); // ✅ Cappuccino IS-A HotDrink cup = new Latte(); // ✅ Latte IS-A HotDrink cup = new Tea(); // ✅ Tea IS-A HotDrink cup = new ArabianCoffee(); // ✅ ArabianCoffee IS-A HotDrink // 🚫 Non-polymorphic cup — only ArabianCoffee! ArabianCoffee arabicCup; arabicCup = new ArabianCoffee(); // ✅ OK // arabicCup = new Latte(); // ❌ ERROR — Latte is not ArabianCoffee!

9 Upcasting & Downcasting

📋 The 4 Parts of an Object Declaration

Before we talk about casting, we must understand the 4 parts of every polymorphic object declaration. Each part plays a different role — and understanding which part belongs to compile time vs runtime is the whole key to casting.

Shape
① Reference
Type
 
s
② Variable
Name
 = 
new
③ Keyword
(creates object)
 
Circle("Red", 5.0);
④ Actual Object
(born at RUNTIME!)
① Reference Type — the label on the box
Controls which methods the compiler allows you to call. A Shape label means only Shape methods are visible — even if the actual object is a Circle.
④ Actual Object — what's really inside
Created in memory when new runs — at runtime only. Java uses this to decide which version of a method to run (dynamic binding).

🆕 The 5 Assignment Scenarios

When you assign one variable to another, the reference types on both sides determine what the compiler allows. There are exactly 5 situations:

✅ Scenario A Polymorphic ← Polymorphic
Scenario A — Shape ← Shape
Shape s1 = new Circle("Red", 5.0); // s1 has reference type Shape Shape s2 = s1; // ✅ Polymorphic ← Polymorphic: Always OK!
Both sides are Shape. Same reference type on both sides. Simple assignment — no casting needed. ✔
✅ Scenario B Polymorphic ← Specific  —  Upcasting (Automatic)
Scenario B — Shape ← Circle (upcasting)
Circle c = new Circle("Red", 5.0); Shape s = c; // ✅ Polymorphic ← Specific: Upcasting! Automatic. // Also works inline in one statement: Shape s2 = new Circle("Blue", 3.0); // ✅ Same thing!
A Circle IS-A Shape — assigning to a broader type always works. Java automatically widens the reference. This is called Upcasting (going up the hierarchy). No cast syntax needed. ✔
⚠️ Scenario C Specific ← Polymorphic  —  Needs Explicit Cast!
Scenario C — Circle ← Shape (must downcast)
Shape s = new Circle("Red", 5.0); // ❌ COMPILE ERROR — compiler refuses without a cast // Circle c = s; // ✅ Downcasting — you tell the compiler: "trust me, it IS a Circle" Circle c = (Circle) s;
The Shape variable could hold any subclass (Circle, Rectangle, Triangle…). The compiler refuses the assignment without an explicit cast. By writing (Circle), you promise the compiler it is a Circle. This is called Downcasting. ⚠️
✅ Scenario D Specific ← Specific (Same Type)
Scenario D — Circle ← Circle
Circle c1 = new Circle("Red", 5.0); Circle c2 = c1; // ✅ Specific ← Specific (same type): Always OK!
Same reference type on both sides. No polymorphism involved — just a normal assignment. ✔
❌ Scenario E Specific ← Specific (Different Type)  —  Compile Error
Scenario E — Circle ← Rectangle (error!)
Rectangle r = new Rectangle("Blue", 4.0, 6.0); // ❌ COMPILE ERROR — Circle is NOT a Rectangle! // Circle c = r; // ❌ STILL a compile error — even with a cast! // Circle c = (Circle) r;
Circle and Rectangle are sibling classes — neither is a subclass of the other. This is always an error, and no cast can fix it. The compiler catches it immediately. ✖
Scenario Left Side (target) Right Side (source) Result
A Shape (poly) Shape variable (poly) ✅ Always OK
B Shape (poly) Circle variable or new Circle() ✅ Upcasting (automatic)
C Circle (specific) Shape variable (poly) ⚠️ Needs (Circle) cast — Downcasting
D Circle (specific) Circle variable (same type) ✅ Always OK
E Circle (specific) Rectangle variable (different type) ❌ Compile Error

🤔 Why Can't the Compiler Just See What's Inside?

Students always ask this question: "I can clearly see on the same line that it's a Circle — why does the compiler complain and demand a cast?"

This is one of the most important things to understand in all of Java. Let us explain it step by step.

👀 AT COMPILE TIME
What the compiler sees:
Shape s = ???;

// Could be any subclass:
// new Circle(...)
// new Rectangle(...)
// new Triangle(...)
// new Pentagon(...)
// new Hexagon(...)
// ...
❌ The compiler only sees the reference type.
It cannot look inside — it has no idea what
object will actually be stored there.
▶︎ AT RUNTIME
What Java sees when it executes:
Shape s = new Circle("Red", 5.0);
✅ Now Java KNOWS the actual object is a Circle!
Objects are only born in memory when
new runs — this happens at runtime.
🧠 The Guard at the Door

Imagine the compiler is a guard at a gate. It sees a box labeled "Shape". It cannot open the box — it can only read the label. When you try to give that box to someone who accepts only "Circle" boxes, the guard says: "I cannot guarantee this box contains a Circle! For all I know, it could be a Rectangle!"

By writing (Circle) s, you tell the guard: "I have personally checked — I promise it is a Circle. Let it through." The guard lets it pass. But if you lied — Java will crash at runtime!

💡 The Core Rule
  • The compiler makes all decisions based on the reference type (left of =)
  • The JVM at runtime knows the actual object type (right of =)
  • Objects are only created when new runs — that is always runtime, never compile time
  • A polymorphic reference hides its contents from the compiler — that is exactly the point of polymorphism!

🎯 Student Challenges

Look at the code first and think about what happens — then reveal the answer:

🤔 Challenge 1 — What happens here?
❌ Compile Error!
Even though we can see s holds a Circle, the compiler only sees the reference type Shape. It refuses to assign a Shape variable to a Circle variable without an explicit cast.
Fix: Circle c = (Circle) s;
🤔 Challenge 2 — Will this crash?
⚠️ Compiles fine — but crashes at runtime!
The cast (Circle) satisfies the compiler. But when Java runs the code, it finds a Rectangle inside — not a Circle. Java throws:
java.lang.ClassCastException: Rectangle cannot be cast to Circle
🤔 Challenge 3 — Upcasting: do you need a cast?
✅ OK — no cast needed!
This is upcasting: assigning a specific type (Triangle) to a polymorphic type (Shape). Since Triangle IS-A Shape, Java does this automatically. No cast syntax required.

Danger ClassCastException — The Runtime Trap

A ClassCastException is a runtime error — your program compiles successfully but crashes when it runs. It happens when you cast to the wrong type.

🚫 ClassCastException — wrong cast at runtime
Shape s = new Rectangle("Blue", 4.0, 6.0); // The compiler is happy — you provided a cast! Circle c = (Circle) s; // ✅ Compiles fine // ❌ But at runtime: ClassCastException! // s actually holds a Rectangle — not a Circle!
🚨 Runtime Error Message:
Exception in thread "main" java.lang.ClassCastException:
    class Rectangle cannot be cast to class Circle

Safe The Solution: Check with instanceof First

Always ask Java: "what type is really inside this variable?" before casting. Use instanceof to check — then cast safely:

✅ Safe downcasting with instanceof
Shape s = new Circle("Red", 5.0); // ✅ Always check FIRST — then cast if (s instanceof Circle) { Circle c = (Circle) s; System.out.println("Diameter: " + c.getDiameter()); } else if (s instanceof Rectangle) { Rectangle r = (Rectangle) s; System.out.println("Width: " + r.getWidth()); } else { System.out.println("Unknown shape type"); }
📚 Coming Next: Generics — No More Casting Errors!

The danger of ClassCastException is real. But there is a solution: in a future chapter, you will learn about Generics (e.g., ArrayList<Circle> instead of ArrayList<Shape>).

Generics let you tell the compiler exactly what type to expect — so mistakes are caught at compile time instead of crashing at runtime. Generics solve the root problem that makes casting dangerous.

📝 Important Clarification: IS-A and Interfaces

We said the condition for polymorphism is the IS-A relationship through inheritance. This is the main rule for this chapter. A Student is not a Shape. A Car is not a Shape. A Book is not a Shape. That is why polymorphism only works within an inheritance hierarchy.

However, you may wonder: what about a Car and a Book? They have nothing in common through inheritance — but both could implement the same interface (for example, Sellable). Interfaces also create an IS-A relationship!

🔒 Rule for this chapter: IS-A means inheritance only. Interfaces are a future topic. The key idea is: polymorphism requires a common type connecting the objects. Whether that connection comes from inheritance or an interface, the concept is the same — but the IS-A from interfaces is a topic for the next chapter.
💡 Casting Summary
  • Poly ← Poly: Always OK (same reference type)
  • Poly ← Specific: Upcasting — Automatic ✔ (IS-A guarantees it)
  • Specific ← Poly: Downcasting — needs explicit (Type) cast ⚠️
  • Specific ← Specific (same): Always OK ✔
  • Specific ← Specific (different): Compile Error ✖
  • Always use instanceof before downcasting to prevent ClassCastException

10 Dynamic Binding — The Engine Behind Polymorphism

🔗 Dynamic Binding & Polymorphism Are Partners
Polymorphism is the ability to treat different objects through a common type. Dynamic Binding is the mechanism that makes it work. Without dynamic binding, polymorphism would be useless — the compiler would always call the superclass version and you would never see the subclass behavior. They are inseparable.

⚖ Static Binding vs Dynamic Binding

Java has two strategies for deciding which method to call. Understanding the difference is the entire story of dynamic binding:

⏰ Static Binding
Early Binding — Compile Time
  • Decision made by the compiler
  • Based on the method signature (arguments)
  • Used for overloaded methods
  • Decided before the program runs
▶︎ Dynamic Binding
Late Binding — Runtime
  • Decision made by the JVM
  • Based on the actual object type
  • Used for overridden methods
  • Decided while the program runs

⏰ Static Binding: The Compiler Decides (Overloading)

Imagine a post office with three counters. The sign says:

Counter 1 — for letters  |  Counter 2 — for parcels  |  Counter 3 — for documents

The guard at the entrance looks at what you are holding and immediately directs you to the right counter — right there, at the entrance, before you even walk in. He does not need to wait. He sees a letter → Counter 1. He sees a parcel → Counter 2. The decision is made at the entrance (compile time) based on what you carry (argument type).

Java's compiler is exactly this guard. When it sees overloaded methods and a call with specific arguments, it resolves the correct method immediately — at compile time.

⏰ Static Binding — overloading, compiler decides
class Printer { void print(int x) { System.out.println("Integer: " + x); } void print(double x) { System.out.println("Double: " + x); } void print(String x) { System.out.println("Text: " + x); } } Printer p = new Printer(); p.print(42); // ← Compiler sees int → calls print(int) — decided NOW p.print(3.14); // ← Compiler sees double → calls print(double) — decided NOW p.print("hello"); // ← Compiler sees String → calls print(String) — decided NOW // The compiler reads the argument types and immediately knows // which overloaded version to call. No waiting. No surprises. // This is STATIC BINDING.
Why is it "static"? Because the binding (connection between the call and the method) is established statically — fixed at compile time, before anything runs. The same answer every time, no matter what.

▶︎ Dynamic Binding: The JVM Decides (Overriding)

Now imagine a different scenario. The guard sees a box labeled "Shape". He looks at the label — but cannot see what is inside.

He cannot send it to "Circle Counter" or "Rectangle Counter" yet — because the label only says "Shape." He stamps it getArea() and sends it to the runtime room.

At runtime, the box is opened. Inside: a Circle. The runtime worker sees it — "Ah! It is a Circle! I know exactly what to do." He runs Circle.getArea(). The decision was made at runtime — this is dynamic binding.

▶︎ Dynamic Binding — overriding, JVM decides at runtime
Shape s; // reference type = Shape s = new Circle("Red", 5.0); s.getArea(); // Compiler: "Shape has getArea() — OK." (no decision on WHICH one) // JVM at runtime: actual object is Circle → calls Circle.getArea() → 78.54 s = new Rectangle("Blue", 4.0, 6.0); s.getArea(); // Compiler: same check. JVM: actual = Rectangle → 24.00 s = new Triangle("Green", 3.0, 4.0); s.getArea(); // Compiler: same check. JVM: actual = Triangle → 6.00 // Same variable 's'. Same call s.getArea(). THREE different results. // The JVM decides WHICH version at runtime. This is DYNAMIC BINDING.
▶︎ Why is it "dynamic"? Because the binding changes dynamically — it is decided fresh at runtime based on what object actually exists in memory. Different object → different result. Same code, different behavior. That is polymorphism.

👥 The Division of Work: Compiler vs JVM

⏰ COMPILER'S JOB
  • Check that the reference type has the method
  • Shape has getArea()? ✅ Allowed.
  • Does NOT decide which version
  • Stamps the call and passes it along
▶︎ JVM'S JOB (at runtime)
  • Look at the actual object in memory
  • Start at the actual class, search UP the hierarchy
  • Find the most specific overridden version
  • Execute that version

🔎 How the JVM Finds the Right Method

When Java sees s.sound() at runtime, it follows this exact lookup path:

Example: Animal a = new Camel();   a.sound();
Step 1: JVM sees actual object is Camel
Step 2: Does Camel override sound()?  → NO — go UP
Step 3: Does Mammal override sound()?  → YES! — use this one
Execute: Mammal.sound()  →  "Warm-blooded mammal sound"
💡 The Golden Rule of Dynamic Binding

Java starts at the actual object's class and searches upward through the hierarchy until it finds the most recent override of that method. If no subclass overrides it, Java eventually reaches and uses the superclass version.

🏭 Practice: The 4-Level Hierarchy

Study this hierarchy carefully. Some classes override sound(), some do not:

📄 The Animal → Mammal → Camel → Hashi hierarchy
class Animal { public void sound() { System.out.println("Generic animal sound"); } } class Mammal extends Animal { @Override public void sound() { System.out.println("Warm-blooded mammal sound"); } } class Camel extends Mammal { // ⚠ Camel does NOT override sound() — it INHERITS Mammal's version! } class Hashi extends Camel { // Hashi = baby camel (حوار) @Override public void sound() { System.out.println("Hwar!"); } }
🏭 Override Map — who has what:
Animal ✅ defines sound() → "Generic animal sound"
extends ↓
Mammal ✅ overrides sound() → "Warm-blooded mammal sound"
extends ↓
Camel ⚠️ NO override → inherits Mammal's
extends ↓
Hashi ✅ overrides sound() → "Hwar!" (baby camel)

🎯 Dynamic Binding Exercises

For each exercise: show the code, discuss it with the class, then reveal the answer.

🤔Exercise 1 — What does this print?
Output: "Hwar!"
Actual object is Hashi (baby camel). JVM checks: does Hashi override sound()? YES — uses Hashi.sound(). The reference type Animal does not matter.
🤔Exercise 2 — What does this print? (careful!)
Output: "Warm-blooded mammal sound"
Actual object is Camel. JVM checks: does Camel override sound()? NOCamel has no override! JVM goes UP to Mammal. Does Mammal override sound()? YES — uses Mammal.sound().
Many students expect "Camel's sound" — but Camel never defined one!
🤔Exercise 3 — Does the reference type change anything?
Output: "Hwar!"
Reference type is Mammal — but the actual object is Hashi (baby camel). Dynamic binding uses the actual object type, not the reference type. Hashi overrides sound()Hashi.sound() runs. The reference type only controls what methods you can call — not which version runs.
🤔Exercise 4 — What about this one?
Output: "Warm-blooded mammal sound"
Actual object is Mammal. Does Mammal override sound()? YES. JVM uses Mammal.sound() immediately — no need to go further up.
🤔Exercise 5 — The full chain — trace each call!
Output:
Generic animal sound  ← Animal (defines, not overridden)
Warm-blooded mammal sound  ← Mammal (overrides Animal)
Warm-blooded mammal sound  ← Camel (no override → goes up to Mammal)
Hwar!  ← Hashi (baby camel, overrides → its own version)
Dynamic binding in full action! Same loop, same call a.sound(), but four different results — each determined at runtime by the actual object type.
💡 Dynamic Binding Summary
  • Static binding: method overloading → compiler decides at compile time based on argument types
  • Dynamic binding: method overriding → JVM decides at runtime based on actual object type
  • The reference type controls what methods you can call
  • The actual object type controls which version runs
  • JVM searches upward from the actual class until it finds an override
  • Dynamic binding is the engine that makes polymorphism work

11 Complete Code Example

Let's put everything together into a complete, working implementation:

◯ Shape.java — Superclass
class Shape { private String color; public Shape(String color) { this.color = color; } public String getColor() { return color; } public String getType() { return "Shape"; } public double getArea() { return 0.0; } @Override public String toString() { return String.format("[%s | %s | area=%.2f]", getType(), color, getArea()); } }
◯ Circle.java  |  ▭ Rectangle.java  |  △ Triangle.java — Subclasses
class Circle extends Shape { private double radius; public Circle(String c, double r) { super(c); radius = r; } @Override public String getType() { return "Circle"; } @Override public double getArea() { return Math.PI * radius * radius; } // Circle-specific — NOT accessible via Shape reference! public double getDiameter() { return 2 * radius; } } class Rectangle extends Shape { private double width, height; public Rectangle(String c, double w, double h) { super(c); width = w; height = h; } @Override public String getType() { return "Rectangle"; } @Override public double getArea() { return width * height; } public double getWidth() { return width; } } class Triangle extends Shape { private double base, height; public Triangle(String c, double b, double h) { super(c); base = b; height = h; } @Override public String getType() { return "Triangle"; } @Override public double getArea() { return 0.5 * base * height; } }
📄 PolymorphismDemo.java — The Main Class
import java.util.ArrayList; public class PolymorphismDemo { // ✅ ONE polymorphic method — accepts any Shape! public static void printInfo(Shape s) { System.out.println(s); // calls s.toString() — dynamic binding! } public static void main(String[] args) { // ✅ Polymorphic ArrayList ArrayList<Shape> shapes = new ArrayList<>(); shapes.add(new Circle("Red", 5.0)); shapes.add(new Rectangle("Blue", 4.0, 6.0)); shapes.add(new Triangle("Green", 3.0, 4.0)); shapes.add(new Circle("Purple", 2.5)); // ✅ ONE polymorphic loop System.out.println("=== All Shapes ==="); for (Shape s : shapes) printInfo(s); // dynamic binding picks the right getArea() // ── Downcasting demo ────────────────────────────── System.out.println("\n=== Circle-Specific Info ==="); for (Shape s : shapes) { if (s instanceof Circle) { Circle c = (Circle) s; // safe downcast System.out.println("Circle diameter: " + c.getDiameter()); } } } }
Output — PolymorphismDemo.java
=== All Shapes === [Circle | Red | area=78.54] [Rectangle | Blue | area=24.00] [Triangle | Green | area=6.00] [Circle | Purple | area=19.63] === Circle-Specific Info === Circle diameter: 10.0 Circle diameter: 5.0

12 Array of Objects

We already know that an array can hold primitive values like int or double. But arrays can also hold objects! When you have many objects of the same class, you can group them all in a single array — this is called an array of objects.

👤 Student[] — Storing Objects in a Fixed Array
class Student { private String name; private int age; public Student(String name, int age) { this.name = name; this.age = age; } public String toString() { return "Student(" + name + ", age=" + age + ")"; } } // ── Create an array of 3 students ──────────────────── Student[] students = new Student[3]; // size is FIXED at 3 students[0] = new Student("Ali", 20); students[1] = new Student("Maha", 21); students[2] = new Student("Omar", 19); // ── Iterate over all students ───────────────────────── for (Student s : students) { System.out.println(s); }

📊 The Array in Memory (capacity = 3):

[0]
👤 Ali
age = 20
[1]
👤 Maha
age = 21
[2]
👤 Omar
age = 19
students[]  |  type: Student[]  |  size: 3  |  fixed forever
⚠︎ The Big Problem — Arrays Are FIXED in Size!

What happens when a 4th student joins? The array is full. Java gives you no way to extend it. The only solution is painful: create a new, bigger array, copy all the old items, then throw the old array away. You have to do this manually — every single time you need more space.

🔄 Manual Resize — The Painful Way
// A 4th student joins! But the array only has 3 slots. // students[3] = new Student("Nora", 22); // ← ERROR! Index out of bounds! // ── Step 1: Create a new, bigger array ─────────────── Student[] bigger = new Student[6]; // ── Step 2: Copy all old items to the new array ─────── for (int i = 0; i < students.length; i++) { bigger[i] = students[i]; } // ── Step 3: Point the reference to the new array ────── students = bigger; // ── Now we can finally add the 4th student ──────────── students[3] = new Student("Nora", 22); // Imagine doing this every time you need more space... 😓

🔄 After Manual Resize (capacity = 6):

[0]
👤 Ali
[1]
👤 Maha
[2]
👤 Omar
[3]
👤 Nora ➕
[4]
null
[5]
null
students[]  |  new capacity: 6  |  resized manually by us
💡 There Has to Be a Better Way!

Isn't there something that handles the resizing automatically so we can just focus on adding our students? Yes — Java gives us ArrayList!

13 ArrayList — The Dynamic Array

📌 Definition — What Is ArrayList?

An ArrayList is a dynamic (flexible) array provided by Java. Unlike a regular array, an ArrayList can grow and shrink automatically as you add or remove items. You never need to worry about running out of space.


It lives in the package:  java.util.ArrayList

Operations What Can We Do with an ArrayList?

➕ add(item)  —  add to end ➖ remove(i)  —  remove by index 🔍 get(i)  —  retrieve item 📊 size()  —  count items 🔄 for loop  —  iterate all
📌 Basic ArrayList Operations
import java.util.ArrayList; // ── Create an ArrayList ──────────────────────────────── ArrayList students = new ArrayList(); // ── ADD items — grows automatically! ────────────────── students.add(new Student("Ali", 20)); students.add(new Student("Maha", 21)); students.add(new Student("Omar", 19)); students.add(new Student("Nora", 22)); // no resize needed — ArrayList handles it! students.add(new Student("Ziad", 20)); // keeps growing! // ── SIZE — how many items? ───────────────────────────── System.out.println("Total students: " + students.size()); // 5 // ── GET — retrieve by index (must cast!) ─────────────── Student first = (Student) students.get(0); System.out.println("First: " + first); // ── ITERATE — loop through all ───────────────────────── for (int i = 0; i < students.size(); i++) { Student s = (Student) students.get(i); System.out.println(s); } // ── REMOVE — removes by index, shifts others left ────── students.remove(1); // removes Maha (index 1) System.out.println("After remove: " + students.size()); // 4
Output
Total students: 5 First: Student(Ali, age=20) Student(Ali, age=20) Student(Maha, age=21) Student(Omar, age=19) Student(Nora, age=22) Student(Ziad, age=20) After remove: 4

🏋︎ Class Discussion — Array or ArrayList?

🌟 The Doctor asks:
My students — what do you think is better: the array or the ArrayList?
👤 Students say:
ArrayList! It grows automatically — no manual copy needed!
💡 The Doctor replies:
Good answer — I love it! But do you know a secret? ArrayList is built on top of a regular fixed array inside Java. The core of ArrayList is still a plain array. ArrayList just manages the resizing for you automatically! And remember — ArrayList is not always fast. Imagine you have a million items and the array fills up — Java must copy all one million items to a new array. That is expensive! So ArrayList is convenient, but not always the fastest choice.

🔌 The Secret Inside ArrayList — How It Really Works

Inside Java, ArrayList secretly uses a regular fixed array to store your data. When that hidden array becomes completely full, it automatically:

  1. Creates a new, bigger array — 1.5× the old size
  2. Copies all old items into the new array
  3. Switches to the new array and discards the old one (garbage collector removes it)

Let's visualize it step by step. Suppose the hidden array starts with capacity 4:

Step 1
📊 3 items stored — capacity 4 is not full yet (75%)
[0]
👤 Ali
[1]
👤 Maha
[2]
👤 Omar
[3]
null
Used: 3 / 4 = 75% ✅ Still has room — no resize yet
75%
Step 2
⚠︎ 4th item added — array is now 100% FULL!
[0]
👤 Ali
[1]
👤 Maha
[2]
👤 Omar
[3]
👤 Nora
Used: 4 / 4 = 100% — FULL! 🚨 Next add will trigger RESIZE!
100% — Full!
Step 3
🔄 5th add triggers RESIZE! New capacity = 4 × 1.5 = 6
OLD array (abandoned):
[0]
Ali
[1]
Maha
[2]
Omar
[3]
Nora
NEW array (capacity = 6):
[0]
Ali
[1]
Maha
[2]
Omar
[3]
Nora
[4]
👤 Ziad ➕
[5]
null

⚡ Java copies all 4 items from the old array to the new one, then adds Ziad in position [4].

Step 4
✅ Done! ArrayList is comfortable again — 5 items in capacity 6 (83%)
[0]
👤 Ali
[1]
👤 Maha
[2]
👤 Omar
[3]
👤 Nora
[4]
👤 Ziad
[5]
null
Used: 5 / 6 = 83% ✅ Comfortable — room to grow!
83%

📌 The old array of capacity 4 is now abandoned. Java's garbage collector removes it from memory automatically.

📌 Real Java Goes Even Further

The real Java ArrayList starts with capacity 10 (not 4), and expands early — when around 90% full — so you never feel a performance pause just before it runs out of space. The factor is still approximately 1.5×. The idea is exactly what we just visualized!

💻 Simple Implementation — Look Inside ArrayList!

Let's write our own simplified ArrayList to see exactly how it works. This is not the real Java source, but it shows the core idea:

⚡ SimpleArrayList.java — ArrayList from Scratch
// Simplified ArrayList — to understand how it really works inside! class SimpleArrayList { private Object[] data; // the secret fixed array inside! private int size; // how many items are actually stored // Constructor — start with a small hidden array public SimpleArrayList() { data = new Object[4]; size = 0; } // Add an item — expands automatically when full public void add(Object item) { if (size == data.length) { // hidden array is FULL → expand! expand(); } data[size] = item; size++; } // Create a bigger array (1.5×), copy old → new, switch private void expand() { int newCapacity = (int)(data.length * 1.5); Object[] newData = new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; // copy old items to new array } data = newData; // switch to the bigger array // old array → abandoned → garbage collector removes it } // Print all stored items public void print() { System.out.println("Size: " + size + " | Capacity: " + data.length); for (int i = 0; i < size; i++) { System.out.println(" [" + i + "] " + data[i]); } } // ── Test ───────────────────────────────────────────── public static void main(String[] args) { SimpleArrayList list = new SimpleArrayList(); list.add("Ali"); list.add("Maha"); list.add("Omar"); System.out.println("--- After 3 items ---"); list.print(); list.add("Nora"); System.out.println("\n--- After 4 items (FULL!) ---"); list.print(); list.add("Ziad"); // ← this add triggers expand()! System.out.println("\n--- After 5 items (auto-expanded!) ---"); list.print(); list.add("Layla"); System.out.println("\n--- After 6 items ---"); list.print(); } }
Output — SimpleArrayList.java
--- After 3 items --- Size: 3 | Capacity: 4 [0] Ali [1] Maha [2] Omar --- After 4 items (FULL!) --- Size: 4 | Capacity: 4 [0] Ali [1] Maha [2] Omar [3] Nora --- After 5 items (auto-expanded!) --- Size: 5 | Capacity: 6 [0] Ali [1] Maha [2] Omar [3] Nora [4] Ziad --- After 6 items --- Size: 6 | Capacity: 6 [0] Ali [1] Maha [2] Omar [3] Nora [4] Ziad [5] Layla
🔍 Did You Notice?

The capacity jumped from 4 to 6 (4 × 1.5 = 6) automatically when we added the 5th item. All that copying happened silently — we just called add() and everything was handled!

🔍 Why Is ArrayList Polymorphic?

📌 The Key Insight — Look at the Internal Array!

Look at the SimpleArrayList code above. The hidden internal array is declared as Object[] data — not Student[] or Car[].


Why Object? Because Object is the superclass of ALL classes in Java! Every class you create automatically inherits from Object (IS-A Object). So an Object[] can hold any type — that is polymorphism!

👥 ArrayList Stores EVERYTHING as Object
ArrayList myList = new ArrayList(); // ALL Java classes are subclasses of Object → ArrayList accepts any of them! myList.add(new Student("Ali", 20)); // Student IS-A Object ✓ myList.add(new Car("Toyota")); // Car IS-A Object ✓ myList.add(new Student("Maha", 21)); // Student IS-A Object ✓ // But when you retrieve, Java only knows it is an "Object" // → you MUST cast to the real type to use its specific methods Student s = (Student) myList.get(0); // downcast needed! Car c = (Car) myList.get(1); // downcast needed!

☝︎ A Student Raises Their Hand...

👤 Student asks:
Doctor, we learned that an array holds only one type. But the ArrayList seems to hold different types (Student, Car...)! Is that not a contradiction?
💡 The Doctor replies:
Excellent question — I love it! You are thinking correctly. ArrayList also holds one type only — and that type is Object! Same rule as always. But since every class in Java is a subclass of Object, everything IS-A Object. So a list of Object can hold any object — that is polymorphism at work!

But there is a cost: when you take something out of the ArrayList, Java only knows it is an Object — not a Student, not a Car. You must cast it back to use the real type. If you store a Student but cast it as a Car — that is a ClassCastException!
🌟 Coming Soon — Generics!
There is a solution: you can tell the ArrayList exactly which type it should hold, so you get compile-time safety and no casting is needed. This powerful feature is called Generics — and we will study it in a future lesson!
🎉 Summary — Array vs ArrayList
Feature Array ArrayList
Size 🔒 Fixed at creation 🔄 Grows / shrinks dynamically
Resize ❌ Must do manually ✅ Automatic
Speed ⚡ Fastest (no copy overhead) 🟠 Good, but copy cost on resize
Built on itself (primitive structure) 🔌 A fixed array internally!
Type stored Declared type (e.g. Student[]) Object — polymorphic!
Retrieval No cast needed Must cast (until Generics)

🎯 Challenge: Trace the ArrayList — What Is the Output?

Study the code below carefully. An ArrayList is built step by step. Before looking at the answer, trace each operation on paper and predict the final output.

📄 ArrayList Operations — Trace the Output
import java.util.ArrayList; ArrayList list = new ArrayList(); list.add(10); // Step 1 list.add(20); // Step 2 list.add(30); // Step 3 list.remove(1); // Step 4 — removes by INDEX list.add(50); // Step 5 list.remove(2); // Step 6 — removes by INDEX list.add(1, 40); // Step 7 — inserts 40 at index 1 System.out.println(list);
🤔 What does System.out.println(list) print?
Hint: remove(int index) removes the element at that index, not the element with that value. And add(int index, value) inserts at a specific position.
📄 Step-by-Step Visualization
Step Operation List State Size
Start new ArrayList() [ empty ] 0
Step 1 add(10) [ 10 ] 1
Step 2 add(20) [ 10, 20 ] 2
Step 3 add(30) [ 10, 20, 30 ] 3
Step 4 remove(1)
20 ← index 1 removed
[ 10, 30 ] 30 shifts left to index 1
2
Step 5 add(50) [ 10, 30, 50 ] 3
Step 6 remove(2)
50 ← index 2 removed
[ 10, 30 ]
2
Step 7 add(1, 40)
Insert 40 at index 1 → 30 shifts right
[ 10, 40, 30 ]
3
✅ Final Output — System.out.println(list)
[10, 40, 30]
⚠ remove(int) — By INDEX
remove(1) removes the element at index 1.
After removal, all elements to the right shift left by one position.
📌 add(index, value) — Insert
add(1, 40) inserts 40 at index 1.
All elements from index 1 onward shift right to make room.
💡 Why is this tricky in the exam?
After Step 4 (remove(1)): the list becomes [10, 30]not [10, 30, ???]. Index 1 was 20, not 30!
After Step 6 (remove(2)): index 2 is now 50 (not 30) — so 50 is removed, leaving [10, 30].
Always re-index the list after every operation before predicting the next step!

Summary — What You Learned Today

🇬🇷
Etymology
Poly (many) + morphe (form). From Greek. Means "many forms."
🧬
Biology Analogy
Group creatures by common type. One treatment for the whole group.
🔑
The Condition
Objects MUST be related by inheritance (IS-A relationship).
🔄
3 Forms
Polymorphic variable, polymorphic array, polymorphic method.
🔍
What You See
Polymorphic ref only sees superclass (shared) methods.
The Cup
General cup = polymorphic. Arabic coffee cup = non-polymorphic.
⬆⬇
Casting
Upcasting is automatic. Downcasting needs explicit cast + instanceof.
🌟
Dynamic Binding
Java picks the method version at RUNTIME based on actual object type.
Good Design
Only add reasonable, shared behaviors to the superclass. Don't force 100%.
📌
ArrayList
A dynamic array built on a fixed array inside. Stores Object → polymorphic. Casting required until Generics.
👤
Array of Objects
Arrays can hold objects — but size is fixed. You must copy manually to resize.
🔄
Dynamic Array
ArrayList hides a fixed array inside. Full? Auto-expand 1.5× and copy. Stores Object → polymorphic!
🏢

Ready to See It All in Action?

You have studied polymorphism, dynamic binding, upcasting, downcasting, and degrees. Now apply everything in a complete real-world staff payroll system — with full Java code, output, casting, and design analysis.

Open Practical Example  →
← Inheritance 🏢 Practical Example Abstract Classes →
🎨 Code Block Theme