Polymorphism in Java - One Interface, Many Forms

⏱️ 23 min read • Beginner Level • Lesson 59

Lesson 59 of 124 of Java Tutorial
You have completed this lesson

Completed on . You can revise this lesson or continue to the next topic.

Polymorphism in Java allows one method name, parent type, or interface type to represent multiple forms of behavior.

Java supports compile-time polymorphism through method overloading and runtime polymorphism through method overriding. Polymorphism helps developers write flexible code that depends on common contracts while concrete objects provide specialized behavior.


What Is Polymorphism in Java?

The word polymorphism means “many forms.” In Java, the same operation can behave differently depending on the method arguments known at compile time or the actual object selected at runtime.

Simple Meaning: The same method call or parent reference can represent different behavior in different situations.
Polymorphism appears when:
  • A class overloads a method with different parameter lists.
  • A child class overrides an inherited instance method.
  • A parent or interface reference points to different concrete objects.
  • Client code calls a common operation without depending on one implementation.

Real-World Analogy

The action “notify a user” can take several forms: email, SMS, or a mobile push notification. The requested action remains the same, but the actual behavior depends on the selected notification service.

Common Operation

send(message)

Different Objects

Email, SMS, and push services.

Different Results

Each object sends the message in its own way.

Types of Polymorphism in Java

TypeAchieved ThroughDecision Time
Compile-time polymorphismMethod overloadingThe compiler selects the matching method.
Runtime polymorphismMethod overriding and upcastingThe JVM selects the overridden instance method.

Compile-Time Polymorphism

Compile-time polymorphism occurs through method overloading. Multiple methods share the same name but have different parameter lists. The compiler selects the most applicable method based on the arguments.

Calculator.java
Copy Try Download
class Calculator {
    int add(int first, int second) {
        return first + second;
    }

    double add(double first, double second) {
        return first + second;
    }

    int add(int first, int second, int third) {
        return first + second + third;
    }

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        System.out.println(calculator.add(10, 20));
        System.out.println(calculator.add(2.5, 3.5));
        System.out.println(calculator.add(1, 2, 3));
    }
}

Output:

30
6.0
6

Explanation

  1. add() is overloaded three times with different parameter lists (types and counts).
  2. When calculator.add(10, 20) is called, the compiler resolves it to the method accepting two ints.
  3. When calculator.add(2.5, 3.5) is called, the compiler resolves it to the method accepting two doubles.
  4. This decision is made entirely at compile time.
Overloading rule: Changing only the return type does not create valid method overloading because the compiler cannot select a method using the return type alone.

Runtime Polymorphism

Runtime polymorphism occurs when a child class overrides an inherited instance method and a parent reference refers to a child object. The actual object decides which overridden method runs.

AnimalExample.java
Copy Try Download
class Animal {
    void makeSound() {
        System.out.println("Animal makes a sound");
    }
}

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

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

class AnimalExample {
    static void playSound(Animal animal) {
        animal.makeSound();
    }

    public static void main(String[] args) {
        playSound(new Dog());
        playSound(new Cat());
    }
}

Output:

Dog says Woof
Cat says Meow

Explanation

  1. playSound() accepts the common parent type.
  2. A Dog or Cat object can be passed to it.
  3. The actual object's overridden method runs.
  4. The client function does not need separate logic for each animal type.

Upcasting

Upcasting stores a child object in a parent-class or interface reference. It is usually implicit and safe because every child object is also an instance of its parent type.

UpcastingExample.java
class Vehicle {
    void move() {
        System.out.println("Vehicle moves");
    }
}

class Car extends Vehicle {
    @Override
    void move() {
        System.out.println("Car drives");
    }

    void openTrunk() {
        System.out.println("Trunk opened");
    }
}

class UpcastingExample {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();
        vehicle.move();

        // vehicle.openTrunk(); // Not visible through Vehicle reference
    }
}

Output:

Car drives

Explanation

  1. Vehicle vehicle = new Car(); is an example of upcasting. A Car object is stored in a Vehicle reference.
  2. Because it's a Vehicle reference, we can call vehicle.move(). The overridden Car.move() executes at runtime.
  3. We cannot call vehicle.openTrunk() because the compiler only allows access to methods defined in the Vehicle reference type.
Reference-type rule: The reference type controls which members are accessible at compile time, while the actual object controls which overridden instance method executes at runtime.

Dynamic Method Dispatch

Dynamic method dispatch is the runtime process used to select an overridden instance method. Java examines the actual object rather than only the reference type.

Parent Reference
Animal animal
Concrete Object
new Dog()
Runtime Method
Dog.makeSound()

Polymorphism with Interfaces

Interface references make polymorphic designs possible even when implementations are otherwise unrelated. Each class agrees to the same contract.

PaymentExample.java
interface PaymentMethod {
    void pay(double amount);
}

class CardPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("Card payment: ₹" + amount);
    }
}

class UpiPayment implements PaymentMethod {
    @Override
    public void pay(double amount) {
        System.out.println("UPI payment: ₹" + amount);
    }
}

class Checkout {
    static void process(PaymentMethod method, double amount) {
        method.pay(amount);
    }

    public static void main(String[] args) {
        process(new CardPayment(), 1500);
        process(new UpiPayment(), 800);
    }
}

Output:

Card payment: ₹1500.0
UPI payment: ₹800.0

Explanation

  1. PaymentMethod is a common interface used as the reference type in process().
  2. Different implementations (CardPayment and UpiPayment) are passed to it.
  3. The pay() behavior changes based on which object is actually passed, demonstrating runtime polymorphism using interfaces.

Polymorphism with Abstract Classes

An abstract class can define common state and behavior while subclasses provide specialized implementations. The abstract parent type can then be used polymorphically.

ShapeExample.java
abstract class Shape {
    abstract double area();
}

class Circle extends Shape {
    private final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    private final double width;
    private final double height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    double area() {
        return width * height;
    }
}

Explanation

  1. Shape is an abstract class defining the area() contract.
  2. Circle and Rectangle provide concrete mathematical implementations for area().
  3. You could write a method printArea(Shape s) that calculates the area without knowing the exact shape at compile time!

Fields Are Not Polymorphic

Instance methods are dynamically dispatched, but fields are resolved using the reference type. A child field with the same name hides the parent field rather than overriding it.

FieldHidingExample.java
class Parent {
    String name = "Parent";

    String getName() {
        return name;
    }
}

class Child extends Parent {
    String name = "Child";

    @Override
    String getName() {
        return name;
    }

    public static void main(String[] args) {
        Parent value = new Child();

        System.out.println(value.name);
        System.out.println(value.getName());
    }
}

Output:

Parent
Child

Explanation

  1. Fields are NOT polymorphic. value.name outputs "Parent" because the reference type is Parent.
  2. Methods ARE polymorphic. value.getName() outputs "Child" because the actual object is Child.
  3. This is known as field hiding instead of field overriding.

Static Methods and Method Hiding

Static methods are associated with classes, not objects. A child static method with the same signature hides the parent method; it does not participate in runtime overriding.

StaticHidingExample.java
class Parent {
    static void show() {
        System.out.println("Parent static method");
    }
}

class Child extends Parent {
    static void show() {
        System.out.println("Child static method");
    }

    public static void main(String[] args) {
        Parent value = new Child();
        value.show();
    }
}

Output:

Parent static method

Explanation

  1. Static methods are resolved at compile time based on the reference type.
  2. Since value is a Parent reference, Parent.show() is executed, not Child.show().
  3. This is called method hiding; static methods cannot be overridden.

Downcasting and instanceof

Downcasting converts a parent reference back to a child reference. It should be used only when the actual object is compatible; otherwise, Java throws ClassCastException.

DowncastingExample.java
class Vehicle {
    void move() {
        System.out.println("Vehicle moves");
    }
}

class Car extends Vehicle {
    void openTrunk() {
        System.out.println("Trunk opened");
    }
}

class DowncastingExample {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();

        if (vehicle instanceof Car car) {
            car.openTrunk();
        }
    }
}

Output:

Trunk opened

Explanation

  1. Vehicle vehicle = new Car(); upcasts the object, hiding openTrunk().
  2. if (vehicle instanceof Car car) safely checks if the object is actually a Car, and if so, downcasts it into the car variable (using Java 16+ pattern matching).
  3. The downcast reference now has access to the child-specific openTrunk() method.
Design note: Frequent downcasting may indicate that the common parent or interface is missing an operation or that responsibilities should be redesigned.

Method Overloading vs Method Overriding

FeatureOverloadingOverriding
Polymorphism typeCompile-timeRuntime
RequirementSame method name, different parameter listInheritance plus compatible method signature
SelectionBased on compile-time argument typesBased on the actual object for instance methods
Return typeCannot differ aloneSame or covariant for reference return types
Static methodsCan be overloadedAre hidden, not overridden
Private methodsCan be overloaded in their classCannot be overridden

Benefits of Polymorphism

Flexible Code

One operation works with many compatible object types.

Loose Coupling

Client code depends on a parent type or interface.

Extensibility

New implementations can be added with fewer client changes.

Testability

Production dependencies can be replaced with test implementations.

Polymorphism Best Practices

  • Program to a meaningful interface or parent type when implementations may vary.
  • Use @Override to help the compiler detect signature mistakes.
  • Respect the behavioral contract of the parent type.
  • Prefer polymorphic method calls over long if/switch chains based on object type.
  • Avoid unnecessary downcasting.
  • Keep overloaded methods clear enough to avoid ambiguous calls.

Tricky Cases

1. Are fields polymorphic?

No. Field access is resolved using the reference type.

2. Are static methods overridden?

No. They are hidden and selected using the reference or class type.

3. Can private methods be overridden?

No. A private method is not inherited as an overridable member.

4. Can final methods be overridden?

No. The final modifier prevents overriding.

5. Can constructors be overridden?

No. Constructors are not inherited, though they can be overloaded.

6. Does changing only the return type overload a method?

No. Overloaded methods require different parameter lists.

7. What determines an overloaded method call?

The compiler uses the method name, argument count, and compile-time argument types to select the most applicable overload.

Common Mistakes

  • Confusing overloading with overriding.
  • Assuming fields are dynamically dispatched.
  • Expecting static methods to use runtime polymorphism.
  • Reducing method visibility while overriding.
  • Forgetting @Override and accidentally creating a different method.
  • Performing unsafe downcasts.
  • Overloading methods with combinations that create ambiguous calls.
  • Using type checks where an appropriate polymorphic operation would be clearer.
Summary:
  • Polymorphism means one operation or reference can take many forms.
  • Method overloading provides compile-time polymorphism.
  • Method overriding provides runtime polymorphism.
  • Upcasting allows parent and interface references to store compatible child objects.
  • The reference type controls visible members at compile time.
  • The actual object controls overridden instance-method execution.
  • Fields and static methods do not use runtime polymorphism.
  • Polymorphic designs improve flexibility, extensibility, coupling, and testing.

Interview Questions ⭐

Polymorphism means one method name, parent type, or interface type can represent multiple forms of behavior.

The main types are compile-time polymorphism, commonly achieved through method overloading, and runtime polymorphism, achieved through method overriding and dynamic method dispatch.

Compile-time polymorphism occurs when the compiler selects an overloaded method based on the method name and the compile-time types, number, and order of arguments.

It is primarily achieved through method overloading.

Runtime polymorphism occurs when a parent-class or interface reference refers to a concrete child object and the object's overridden instance method is selected at runtime.

It is achieved through inheritance or interface implementation, method overriding, and a parent or interface reference to a compatible object.

Method overloading defines multiple methods with the same name but different parameter lists.

No. The parameter list must differ because the return type alone is not used to select an overloaded method.

Method overriding occurs when a child class supplies a compatible implementation of an inherited instance method.

Dynamic method dispatch is the runtime process that selects an overridden instance method according to the actual object.

Upcasting stores a child object in a parent-class or interface reference. It is usually implicit and safe.

The compile-time reference type determines which members are accessible.

The actual runtime object determines which overridden instance method executes.

No. Field access is resolved using the reference type, and a child field with the same name hides rather than overrides the parent field.

Static methods do not participate in runtime polymorphism. A child static method hides the parent static method rather than overriding it.

No. Private methods are not inherited as overridable members.

No. The final modifier prevents overriding.

No. Constructors are not inherited, although a class can overload its own constructors.

No. An overriding method may preserve or widen accessibility, but it cannot reduce it.

A covariant return type allows an overriding method to return a subtype of the reference type returned by the parent method.

Downcasting explicitly converts a parent reference to a compatible child reference so child-specific members can be accessed.

Java throws ClassCastException at runtime.

The instanceof operator checks whether the actual object is compatible with the target type and helps prevent ClassCastException.

An interface reference can refer to objects of different implementing classes, and each object's implementation of the interface method can execute at runtime.

An abstract-class reference can refer to different concrete subclasses whose overridden methods provide specialized behavior.

Overloading uses the same method name with different parameter lists and is resolved at compile time. Overriding replaces inherited instance behavior and is resolved at runtime for polymorphic calls.

The compiler selects an overloaded method using the compile-time types of the reference and arguments.

Yes. Static methods can be overloaded because overloading is based on different parameter lists.

Yes. Private methods can be overloaded within their declaring class because overloading does not require inheritance.

The Override annotation asks the compiler to verify that a method correctly overrides or implements an inherited method, helping detect signature mistakes.

Polymorphism improves flexibility, extensibility, loose coupling, code reuse, maintainability, and testability by allowing code to depend on common contracts.

Program to a meaningful interface or parent type when implementations may vary, and avoid unnecessary type checks and downcasts.

Next step: Master in Final Keyword

Continue to Final Keyword →

Test your understanding with a quick quiz



🚀 Quick Knowledge Check

Topic: Polymorphism | Language: Java

Question 1 of 25
Q1. Why use instanceof before downcasting?
Q2. Which is the best polymorphism design guideline?
Q3. What is the output of this static-method example?
class Parent {
    static void show() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    static void show() {
        System.out.println("Child");
    }
}

class Test {
    public static void main(String[] args) {
        Parent value = new Child();
        value.show();
    }
}
Q4. What can an invalid downcast cause?
Q5. What is the output of this overloaded-method example?
class Printer {
    void print(int value) {
        System.out.println("int");
    }

    void print(double value) {
        System.out.println("double");
    }

    public static void main(String[] args) {
        new Printer().print(10);
    }
}
Q6. Which statement correctly compares overloading and overriding?
Q7. What happens if an overriding method reduces the parent method's visibility?
Q8. What is dynamic method dispatch?
Q9. What does polymorphism mean in Java?
Q10. Can methods be overloaded by changing only the return type?
Q11. What is downcasting?
Q12. Can static methods be overloaded?
Q13. Which mechanism provides runtime polymorphism?
Q14. How do interfaces support polymorphism?
Q15. Are static methods overridden?
Q16. Can a final method be overridden?
Q17. What controls which members are accessible through a reference?
Q18. Can constructors be overridden?
Q19. Are instance fields dynamically dispatched?
Q20. What is the output of this runtime-polymorphism example?
class Animal {
    void sound() {
        System.out.println("Animal");
    }
}

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

class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.sound();
    }
}
Q21. Which mechanism provides compile-time polymorphism?
Q22. What is the output of this field-hiding example?
class Parent {
    String name = "Parent";
}

class Child extends Parent {
    String name = "Child";
}

class Test {
    public static void main(String[] args) {
        Parent value = new Child();
        System.out.println(value.name);
    }
}
Q23. Can a private method be overridden?
Q24. What is upcasting?
Q25. What controls which overridden instance method executes?

Great job! Continue learning Java OOP step by step.

Discussion

Ask questions, share suggestions, or discuss this lesson.

Please login or create an account to join the discussion and save your learning activity.

Loading comments...

Have you completed this lesson?

Mark this lesson as completed to track your learning progress. To keep track across devices, please login and save your learning progress.

Not completed yet.