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 = newCircle[3];
Rectangle[] rectangles = newRectangle[3];
Triangle[] triangles = newTriangle[3];
Square[] squares = newSquare[3];
Pentagon[] pentagons = newPentagon[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 voiddisplay(Circle c) {
System.out.println(c.getArea());
}
static voiddisplay(Rectangle r) {
System.out.println(r.getArea());
}
static voiddisplay(Triangle t) {
System.out.println(t.getArea());
}
static voiddisplay(Square s) {
System.out.println(s.getArea());
}
static voiddisplay(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 = newShape[6];
shapes[0] = newCircle("Red", 5.0);
shapes[1] = newRectangle("Blue", 4.0, 6.0);
shapes[2] = newTriangle("Green", 3.0, 4.0);
shapes[3] = newCircle("Pink", 2.5);
shapes[4] = newRectangle("Gold", 8.0, 2.0);
shapes[5] = newTriangle("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 voiddisplay(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 CircleextendsShape,
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.
Shape s = newCircle(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!
🔄 The Polymorphic Cup
A general cup — can hold any hot drink!
CappuccinoLatteTeaHot Choc.Arabic Coffee
Many types ✓
🚫 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 = newCappuccino(); // ✅ Cappuccino IS-A HotDrink
cup = newLatte(); // ✅ Latte IS-A HotDrink
cup = newTea(); // ✅ Tea IS-A HotDrink
cup = newArabianCoffee(); // ✅ ArabianCoffee IS-A HotDrink// 🚫 Non-polymorphic cup — only ArabianCoffee!ArabianCoffee arabicCup;
arabicCup = newArabianCoffee(); // ✅ 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:
Both sides are Shape. Same reference type on both sides. Simple assignment — no casting needed. ✔
✅ Scenario BPolymorphic ← Specific — Upcasting (Automatic)
Scenario B — Shape ← Circle (upcasting)▼
Circle c = newCircle("Red", 5.0);
Shape s = c; // ✅ Polymorphic ← Specific: Upcasting! Automatic.// Also works inline in one statement:Shape s2 = newCircle("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. ✔
Shape s = newCircle("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 DSpecific ← Specific (Same Type)
Scenario D — Circle ← Circle▼
Circle c1 = newCircle("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 ESpecific ← Specific (Different Type) — Compile Error
Scenario E — Circle ← Rectangle (error!)▼
Rectangle r = newRectangle("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?
👁︎ Show Code▼
Shape s = newCircle("Red", 5.0);
Circle c = s;
❌ 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?
👁︎ Show Code▼
Shape s = newRectangle("Blue", 4.0, 6.0);
Circle c = (Circle) s;
⚠️ 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?
👁︎ Show Code▼
Triangle t = newTriangle("Green", 6.0, 4.0);
Shape s = t;
✅ 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 = newRectangle("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 = newCircle("Red", 5.0);
// ✅ Always check FIRST — then castif (s instanceofCircle) {
Circle c = (Circle) s;
System.out.println("Diameter: " + c.getDiameter());
} else if (s instanceofRectangle) {
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.
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▼
classPrinter {
voidprint(int x) {
System.out.println("Integer: " + x);
}
voidprint(double x) {
System.out.println("Double: " + x);
}
voidprint(String x) {
System.out.println("Text: " + x);
}
}
Printer p = newPrinter();
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 = newCircle("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 = newRectangle("Blue", 4.0, 6.0);
s.getArea(); // Compiler: same check. JVM: actual = Rectangle → 24.00
s = newTriangle("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
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:
For each exercise: show the code, discuss it with the class, then reveal the answer.
🤔Exercise 1 — What does this print?
👁︎ Show Code▼
Animal a = newHashi();
a.sound();
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!)
👁︎ Show Code▼
Animal a = newCamel();
a.sound();
Output: "Warm-blooded mammal sound"
Actual object is Camel. JVM checks: does Camel override sound()?
NO — Camel 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?
👁︎ Show Code▼
Mammal m = newHashi();
m.sound();
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?
👁︎ Show Code▼
Animal a = newMammal();
a.sound();
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.
import java.util.ArrayList;
public classPolymorphismDemo {
// ✅ ONE polymorphic method — accepts any Shape!public static voidprintInfo(Shape s) {
System.out.println(s); // calls s.toString() — dynamic binding!
}
public static voidmain(String[] args) {
// ✅ Polymorphic ArrayListArrayList<Shape> shapes = newArrayList<>();
shapes.add(newCircle("Red", 5.0));
shapes.add(newRectangle("Blue", 4.0, 6.0));
shapes.add(newTriangle("Green", 3.0, 4.0));
shapes.add(newCircle("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 instanceofCircle) {
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▼
classStudent {
privateString name;
privateint age;
publicStudent(String name, int age) {
this.name = name;
this.age = age;
}
publicStringtoString() {
return"Student(" + name + ", age=" + age + ")";
}
}
// ── Create an array of 3 students ────────────────────Student[] students = newStudent[3]; // size is FIXED at 3
students[0] = newStudent("Ali", 20);
students[1] = newStudent("Maha", 21);
students[2] = newStudent("Omar", 19);
// ── Iterate over all students ─────────────────────────for (Student s : students) {
System.out.println(s);
}
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 = newStudent[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] = newStudent("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 = newArrayList();
// ── ADD items — grows automatically! ──────────────────
students.add(newStudent("Ali", 20));
students.add(newStudent("Maha", 21));
students.add(newStudent("Omar", 19));
students.add(newStudent("Nora", 22)); // no resize needed — ArrayList handles it!
students.add(newStudent("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:
Creates a new, bigger array — 1.5× the old size
Copies all old items into the new array
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!
⚡ 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!classSimpleArrayList {
privateObject[] data; // the secret fixed array inside!privateint size; // how many items are actually stored// Constructor — start with a small hidden arraypublicSimpleArrayList() {
data = newObject[4];
size = 0;
}
// Add an item — expands automatically when fullpublic voidadd(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, switchprivate voidexpand() {
int newCapacity = (int)(data.length * 1.5);
Object[] newData = newObject[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 itemspublic voidprint() {
System.out.println("Size: " + size + " | Capacity: " + data.length);
for (int i = 0; i < size; i++) {
System.out.println(" [" + i + "] " + data[i]);
}
}
// ── Test ─────────────────────────────────────────────public static voidmain(String[] args) {
SimpleArrayList list = newSimpleArrayList();
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 = newArrayList();
// ALL Java classes are subclasses of Object → ArrayList accepts any of them!
myList.add(newStudent("Ali", 20)); // Student IS-A Object ✓
myList.add(newCar("Toyota")); // Car IS-A Object ✓
myList.add(newStudent("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 methodsStudent 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 = newArrayList();
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).
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.