LLD
Machine Coding
Interview Course
Java โ€ข Interview Prep
๐Ÿงฑ OOP Fundamentals/

Polymorphism: One Interface, Many Forms

Lesson 4 of 8

Polymorphism: One Interface, Many Forms


What is it? (The Analogy)

Imagine you are a music conductor standing in front of an orchestra. You raise your baton and give the signal: "Play!" Now, the violinist draws the bow across strings. The drummer strikes the drums. The pianist presses keys. The flutist blows into a tube. Every musician responds to the SAME command -- "Play!" -- but each one does it in their own way. You, the conductor, do not need to know HOW each instrument works. You just say "Play!" and the right thing happens.

THAT is polymorphism. The word literally comes from Greek: "poly" (many) + "morph" (forms). One command, many forms of execution. In Java, you can write code that says \instrument.play()\ and it works correctly whether the object is a Guitar, Piano, or Drums -- WITHOUT you having to check what type it is. The object itself knows how to play. You just tell it to.

This is arguably the most powerful concept in all of Object-Oriented Programming. It is the reason you can write flexible, extensible code that does not break when you add new types. It is the reason frameworks like Spring and Android can work with YOUR classes even though they were written years before your classes existed. Once you truly understand polymorphism, everything in OOP "clicks."


Why do we need it?

Imagine you run a zoo and you need to feed all the animals. Without polymorphism, your code looks like this:

java
1// WITHOUT polymorphism -- the "type-checking nightmare"
2public void feedAnimal(Object animal) {
3    if (animal instanceof Dog) {
4        Dog d = (Dog) animal;
5        d.eatKibble();
6    } else if (animal instanceof Cat) {
7        Cat c = (Cat) animal;
8        c.eatFish();
9    } else if (animal instanceof Parrot) {
10        Parrot p = (Parrot) animal;
11        p.eatSeeds();
12    } else if (animal instanceof Snake) {
13        Snake s = (Snake) animal;
14        s.eatMouse();
15    }
16    // Add a new animal? Must update EVERY if-else chain EVERYWHERE!
17    // Have 50 animal types? FIFTY branches! This is insanity!
18}

Every time you add a new animal, you have to find EVERY place in the code with these if-else chains and update them. Miss one? Runtime bug.

With polymorphism:

java
1// WITH polymorphism -- elegant, extensible, beautiful!
2public void feedAnimal(Animal animal) {
3    animal.eat();  // The RIGHT eat() method runs automatically!
4}
5
6// Add 100 new animal types? This method NEVER changes.
7// Each animal knows how to eat itself.

ZERO if-else chains. ZERO type checking. Adding new animals requires ZERO changes to existing code. This is the Open/Closed Principle in action.


How it works -- Step by Step

  1. Define a common type -- A superclass or interface with a method signature (like \play()\).
  2. Create subclasses that override -- Each child class provides its own version of that method.
  3. Use the parent type as the reference -- Declare variables, parameters, and return types as the PARENT type.
  4. Java figures out which version to call at runtime -- This is called dynamic dispatch. Java looks at the actual object type (not the reference type) to decide which method to execute.

Two types of polymorphism in Java:

TypeAlso CalledMechanismWhen Resolved
Compile-timeStatic / OverloadingSame method name, different paramsCompile time
RuntimeDynamic / OverridingSame method signature, different classRuntime

Let's Build It Together

Let's build a shape drawing system that showcases the POWER of polymorphism!

java
1// The parent type -- defines the "contract"
2public class Shape {
3    private String color;
4
5    public Shape(String color) {
6        this.color = color;
7    }
8
9    // This will be OVERRIDDEN by each shape
10    public double area() {
11        return 0;  // Default: unknown shape has no area
12    }
13
14    // This too
15    public void draw() {
16        System.out.println("Drawing a shape...");
17    }
18
19    public String getColor() { return color; }
20}
java
1public class Circle extends Shape {
2    private double radius;
3
4    public Circle(String color, double radius) {
5        super(color);
6        this.radius = radius;
7    }
8
9    @Override
10    public double area() {
11        return Math.PI * radius * radius;
12    }
13
14    @Override
15    public void draw() {
16        System.out.println("Drawing a " + getColor() + " circle with radius " + radius);
17    }
18}
java
1public class Rectangle extends Shape {
2    private double width;
3    private double height;
4
5    public Rectangle(String color, double width, double height) {
6        super(color);
7        this.width = width;
8        this.height = height;
9    }
10
11    @Override
12    public double area() {
13        return width * height;
14    }
15
16    @Override
17    public void draw() {
18        System.out.println("Drawing a " + getColor() + " rectangle ("
19            + width + " x " + height + ")");
20    }
21}
java
1public class Triangle extends Shape {
2    private double base;
3    private double height;
4
5    public Triangle(String color, double base, double height) {
6        super(color);
7        this.base = base;
8        this.height = height;
9    }
10
11    @Override
12    public double area() {
13        return 0.5 * base * height;
14    }
15
16    @Override
17    public void draw() {
18        System.out.println("Drawing a " + getColor() + " triangle with base "
19            + base + " and height " + height);
20    }
21}

Now watch the magic:

java
1public class PolymorphismDemo {
2    public static void main(String[] args) {
3
4        // UPCASTING -- storing child objects in parent-type variables
5        // The reference type is Shape, but the ACTUAL objects are different!
6        Shape s1 = new Circle("Red", 5.0);
7        Shape s2 = new Rectangle("Blue", 4.0, 6.0);
8        Shape s3 = new Triangle("Green", 3.0, 8.0);
9
10        // Polymorphism in action -- each calls its OWN version!
11        s1.draw();  // "Drawing a Red circle with radius 5.0"
12        s2.draw();  // "Drawing a Blue rectangle (4.0 x 6.0)"
13        s3.draw();  // "Drawing a Green triangle with base 3.0 and height 8.0"
14
15        // Put them in an array of the parent type
16        Shape[] shapes = { s1, s2, s3 };
17
18        // THIS is the real power -- one loop handles ALL shape types
19        double totalArea = 0;
20        for (Shape shape : shapes) {
21            shape.draw();                  // Polymorphic call!
22            double a = shape.area();       // Polymorphic call!
23            System.out.println("Area: " + String.format("%.2f", a));
24            totalArea += a;
25        }
26        System.out.println("Total area of all shapes: "
27            + String.format("%.2f", totalArea));
28
29        // THE BEST PART: Adding a new shape (like Pentagon) requires
30        // ZERO changes to any of this code!
31    }
32
33    // This method works with ANY shape -- current and FUTURE ones!
34    public static void printShapeInfo(Shape shape) {
35        System.out.println("Shape: " + shape.getClass().getSimpleName());
36        System.out.println("Color: " + shape.getColor());
37        System.out.println("Area: " + String.format("%.2f", shape.area()));
38        shape.draw();
39    }
40}

The Big Aha! Look at the variable \s1\: its reference type is \Shape\, but its object type is \Circle\. When you call \s1.draw()\, Java does NOT look at the reference type. It looks at the actual object in memory and runs Circle's \draw()\. This "late binding" / "dynamic dispatch" is the engine that powers polymorphism. The decision of which method to run happens at RUNTIME, not compile time.


Visual Mental Model

java
1    COMPILE TIME                         RUNTIME
2    (what the compiler sees)             (what actually happens)
3
4    Shape s1 = new Circle("Red", 5);
5
6    s1 is type Shape                     s1 actually points to a Circle
7         |                                      |
8         v                                      v
9    The compiler only lets you           Java looks at the REAL object
10    call methods defined in Shape.       and runs Circle's version.
11    (area, draw, getColor)               draw() -> Circle.draw()
12
13    +-------+         +---------+
14    | s1    |-------->| Circle  |
15    | Shape |   ref   | color   |
16    +-------+         | radius  |
17                      | draw()  | <-- THIS is what actually runs!
18                      | area()  | <-- THIS is what actually runs!
19                      +---------+
20
21    Reference type determines           Object type determines
22    WHAT you can call                   HOW it executes
23    (compile-time check)                (runtime dispatch)

Overloading vs Overriding at a Glance:

java
1    OVERLOADING (Compile-time)          OVERRIDING (Runtime)
2    Same class, different params:       Parent + child, same params:
3
4    class Calculator {                  class Animal {
5        int add(int a, int b)              void speak() { "..." }
6        double add(double a, double b)  }
7        int add(int a, int b, int c)    class Dog extends Animal {
8    }                                       void speak() { "Woof!" }
9                                        }
10
11    Resolved at COMPILE time            Resolved at RUNTIME
12    by looking at arguments.            by looking at actual object.

Real-World Analogy Recap

Polymorphism is like a universal power adapter. You have one socket (the parent type), and different devices (subclasses) plug into it. The socket does not care if it is powering a phone, a laptop, or a toaster. It just provides electricity. Each device uses that electricity in its own way. The socket's "contract" is simple: "I provide power." What each device does with it is its own business. Adding a new device (a new subclass) requires zero changes to the socket.


Common Mistakes & Gotchas

  • Confusing reference type with object type: \Shape s = new Circle(...)\ -- you can only call Shape methods on \s\, even though the object is a Circle. To call Circle-specific methods, you need to downcast: \((Circle) s).getRadius()\. But avoid downcasting when possible -- it defeats the purpose of polymorphism.
  • Thinking overloading IS polymorphism: While technically "compile-time polymorphism," overloading is really just method name reuse. The TRUE power of polymorphism is in runtime overriding. Interviewers will judge you if you confuse them.
  • Not understanding dynamic dispatch: When you call \shape.area()\, Java does NOT look at the declared type of the variable. It looks at the actual object in the heap. This happens at runtime, not compile time.
  • Forgetting that fields are NOT polymorphic: Only methods are dynamically dispatched. If both parent and child have a field called \name\, the one used depends on the REFERENCE type, not the object type. This is called "field hiding" and it is almost always a bug.
  • Breaking Liskov Substitution: A child class should be usable EVERYWHERE the parent is used. If your \Penguin.fly()\ throws an exception because penguins cannot fly, your design is wrong. The child must honor the parent's contract.

Interview Tip

Polymorphism is THE most-asked OOP concept in interviews. Be ready to explain: "Runtime polymorphism allows a parent reference to invoke overridden methods on child objects, with the actual method determined at runtime via dynamic dispatch. This enables writing code against abstractions rather than concrete types, making the system extensible without modification (Open/Closed Principle)." Bonus: draw the reference-vs-object diagram on the whiteboard.


Quick Quiz

  1. Given \Shape s = new Circle("Red", 5)\, what happens if you write \s.getRadius()\? Why? How would you fix it?
  2. If you add a new \Pentagon\ class that extends \Shape\, does the \printShapeInfo(Shape shape)\ method need any changes? Why is this powerful?
  3. In the overloading example, if you call \calculator.add(3, 4)\, which version runs? What about \calculator.add(3.0, 4.0)\? When is this decided?

Summary -- Key Takeaways

  • Polymorphism means one interface, many implementations. A parent reference can point to any child object, and the correct overridden method runs at runtime.
  • Runtime polymorphism (overriding) is the real deal -- it enables extensible, flexible designs. Compile-time polymorphism (overloading) is just method name reuse.
  • Dynamic dispatch means Java decides which method to call based on the actual object type at runtime, not the reference type at compile time.
  • This is arguably the most powerful OOP concept because it allows you to write code that works with types that DO NOT EXIST YET.