Board Formulas
β˜• JavaπŸ“… Day 15

Polymorphism in Java

One name, many forms: compile-time polymorphism with overloading, run-time polymorphism with overriding, upcasting and dynamic method dispatch.

🎯

Learning objectives

  • β†’Explain compile-time (overloading) and run-time (overriding) polymorphism
  • β†’Store a subclass object in a parent-type reference (upcasting)
  • β†’Predict which overridden method runs under dynamic method dispatch
  • β†’Downcast safely with instanceof

πŸ’‘ Key points

  • Polymorphism means many forms: the same method call can behave differently depending on the object.
  • Compile-time polymorphism is method overloading. The compiler picks the version from the argument types before the program runs.
  • Run-time polymorphism is method overriding. The JVM picks the version from the actual object while the program runs.
  • Upcasting means a parent reference points to a child object, as in Animal a = new Dog(). It is automatic and always safe.
  • Dynamic method dispatch: after Animal a = new Dog(), a.sound() runs Dog's sound(), because the object really is a Dog.
  • The reference type decides which methods you are allowed to call. The object type decides which version actually runs.
  • Downcasting (Dog d = (Dog) a;) needs an explicit cast. Check with instanceof first to avoid a ClassCastException.

πŸ’» Code examples(5)

#1Compile-time polymorphism: overloading
java
class Printer {
    void print(int n) {
        System.out.println("int: " + n);
    }

    void print(double d) {
        System.out.println("double: " + d);
    }

    void print(String s) {
        System.out.println("String: " + s);
    }
}

public class OverloadDemo {
    public static void main(String[] args) {
        Printer p = new Printer();
        p.print(10);
        p.print(9.5);
        p.print("Amit");
        p.print('A');     // no print(char) exists
    }
}
Output
int: 10
double: 9.5
String: Amit
int: 65
The compiler reads each argument's type and picks the matching version. There is no print(char), so 'A' is widened to the closest match, int, and its code 65 is printed.
#2Upcasting and dynamic method dispatch
java
class Animal {
    void sound() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Woof");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow");
    }
}

public class Dispatch {
    public static void main(String[] args) {
        Animal a;          // parent-type reference

        a = new Dog();     // upcasting
        a.sound();

        a = new Cat();
        a.sound();

        a = new Animal();
        a.sound();
    }
}
Output
Woof
Meow
Some sound
The same line a.sound() runs three different methods. Java looks at the object a points to at that moment, not at the declared type Animal. This is dynamic method dispatch.
#3One array, many shapes
java
class Shape {
    String name;
    Shape(String name) { this.name = name; }
    double area() { return 0; }
}

class Rectangle extends Shape {
    double l, w;
    Rectangle(double l, double w) {
        super("Rectangle");
        this.l = l;
        this.w = w;
    }
    @Override
    double area() { return l * w; }
}

class Triangle extends Shape {
    double b, h;
    Triangle(double b, double h) {
        super("Triangle");
        this.b = b;
        this.h = h;
    }
    @Override
    double area() { return 0.5 * b * h; }
}

public class ShapeDemo {
    public static void main(String[] args) {
        Shape[] shapes = {
            new Rectangle(4, 5),
            new Triangle(6, 4),
            new Rectangle(3, 3)
        };
        double total = 0;
        for (Shape s : shapes) {
            System.out.println(s.name + ": " + s.area());
            total += s.area();
        }
        System.out.println("Total area: " + total);
    }
}
Output
Rectangle: 20.0
Triangle: 12.0
Rectangle: 9.0
Total area: 41.0
The loop only knows about Shape, yet each element runs its own area(). If you add a Circle class later, this loop works without changing a single line.
#4Reference type limits what you can call
java
class Animal {
    void eat() {
        System.out.println("Eating");
    }
}

class Dog extends Animal {
    void fetch() {
        System.out.println("Fetching the ball");
    }
}

public class CastDemo {
    public static void main(String[] args) {
        Animal a = new Dog();       // upcast
        a.eat();
        // a.fetch();  // error: Animal has no fetch()

        if (a instanceof Dog) {
            Dog d = (Dog) a;        // downcast
            d.fetch();
        }

        Animal plain = new Animal();
        System.out.println(plain instanceof Dog);
    }
}
Output
Eating
Fetching the ball
false
The compiler only allows methods that exist in the reference type, Animal. To call fetch() you downcast back to Dog. Check with instanceof first, because casting a plain Animal to Dog throws ClassCastException.
#5A polymorphic parameter: billing
java
class Customer {
    String name;
    Customer(String name) { this.name = name; }
    double discount() { return 0.0; }
}

class PrimeCustomer extends Customer {
    PrimeCustomer(String name) { super(name); }
    @Override
    double discount() { return 0.10; }
}

public class Billing {
    static void printBill(Customer c, double amount) {
        double pay = amount - amount * c.discount();
        System.out.println(c.name + " pays Rs." + pay);
    }

    public static void main(String[] args) {
        printBill(new Customer("Amit"), 2000);
        printBill(new PrimeCustomer("Priya"), 2000);
    }
}
Output
Amit pays Rs.2000.0
Priya pays Rs.1800.0
printBill() accepts any Customer, including subclasses, because passing a PrimeCustomer upcasts it. c.discount() picks the right version at run time, so one method handles every kind of customer.

🎯 Practice

Q1. Is method overloading compile-time or run-time polymorphism?+

Compile-time. The compiler chooses the overloaded version from the argument types. Overriding is run-time polymorphism, where the JVM chooses based on the actual object.

Q2. Animal a = new Cat(); and Cat overrides sound(). Which sound() runs on a.sound()?+

Cat's sound(). The object is a Cat, and overridden methods are chosen by the object's type (dynamic method dispatch), not by the reference type.

Q3. Why does Animal a = new Dog(); a.fetch(); fail to compile when only Dog has fetch()?+

The compiler checks every call against the reference type. Animal has no fetch(), so it reports 'cannot find symbol'. Downcast first: ((Dog) a).fetch();

Q4. What happens here? Animal a = new Animal(); Dog d = (Dog) a;+

It compiles, but it throws ClassCastException when it runs, because the object is not a Dog. Guard the cast with if (a instanceof Dog).

Q5. Give two differences between overloading and overriding.+

Overloading uses the same name with different parameters, usually in the same class, and is decided at compile time. Overriding uses the same name and the same parameters in a subclass, and is decided at run time.

πŸ“ Notes

Overloading vs overriding

OverloadingOverriding
WhereUsually in one classParent and child class
ParametersMust differMust be the same
DecidedAt compile time, from argument typesAt run time, from the object
Also calledCompile-time polymorphismRun-time polymorphism
AnnotationNone@Override (optional, recommended)
Access ruleNoneCannot be more restricted than the parent's

What is not polymorphic

Only overridable instance methods use dynamic dispatch. static, private and final methods are fixed at compile time, and so are fields:

class A { String tag = "A"; }
class B extends A { String tag = "B"; }

A obj = new B();
System.out.println(obj.tag);   // A, not B

Fields follow the reference type, not the object. This is one more reason to keep fields private and reach them through methods, which are overridden.

A shortcut you may see

From Java 16, instanceof can test and cast in one step:

if (a instanceof Dog d) {
    d.fetch();
}

The plain instanceof with a cast, as in example 4, works in every Java version.

Common mistakes

  • Calling a child-only method through a parent reference. Upcasting hides those methods until you downcast.
  • Casting without checking. That leads to ClassCastException at run time.
  • Lowering access when overriding. Overriding a public method without public gives f() in Q cannot override f() in P (attempting to assign weaker access privileges).
  • Thinking you have overridden when you have overloaded. void sound(String s) in Dog is a new, overloaded method, not an override of sound(). @Override catches this.

Next: Day 16, Abstract Classes and Interfaces. You will write classes that define what must be done and leave how to subclasses.