LearnContact
Lesson 2350 min read

Polymorphism in Java

Learn Polymorphism in Java in detail, including compile-time and runtime polymorphism, method overloading, method overriding, dynamic method dispatch, upcasting, covariant return types, and practical examples.

Introduction

Polymorphism is one of the fundamental principles of Object-Oriented Programming. It allows the same method call, reference type, or interface to represent different forms of behavior.

Simple Polymorphism Example
class Animal {

    void sound() {

        System.out.println(
            "Animal makes a sound"
        );

    }

}


class Dog extends Animal {

    @Override
    void sound() {

        System.out.println(
            "Dog barks"
        );

    }

}


class Cat extends Animal {

    @Override
    void sound() {

        System.out.println(
            "Cat meows"
        );

    }

}
Same Reference Type, Different Behavior
Animal animal;


animal = new Dog();

animal.sound();


animal = new Cat();

animal.sound();
Output
Dog barks

Cat meows

The same Animal reference is used with different child objects. The sound() method behaves differently depending on the actual object.

What You Will Learn
  • What polymorphism is.
  • Why polymorphism is useful.
  • The types of polymorphism in Java.
  • What compile-time polymorphism is.
  • How method overloading works.
  • Rules of method overloading.
  • How automatic type promotion affects overloaded methods.
  • What ambiguous method calls are.
  • What runtime polymorphism is.
  • How method overriding enables runtime polymorphism.
  • The difference between reference type and object type.
  • What dynamic method dispatch is.
  • How upcasting enables polymorphism.
  • The limitations of parent references.
  • How downcasting works.
  • How instanceof works.
  • How pattern matching with instanceof works.
  • How to create polymorphic arrays.
  • How to use polymorphic method parameters.
  • How to use polymorphic return types.
  • What covariant return types are.
  • How abstract classes support polymorphism.
  • How interfaces support polymorphism.
  • How polymorphism supports dependency injection.
  • Why static methods do not provide runtime polymorphism.
  • Why fields are not polymorphic.
  • The difference between overloading and overriding.
  • The difference between early and late binding.
  • How to design flexible applications using polymorphism.

What is Polymorphism?

The word polymorphism means many forms. In Java, polymorphism allows one method name, parent type, abstract class, or interface to represent different implementations and behaviors.

Meaning of Polymorphism
POLY

Many


MORPH

Forms


POLYMORPHISM

Many Forms
Core Idea
SAME METHOD CALL

        sound()

           │
           ▼

    ┌──────────────┐
    │ Actual Object │
    └──────────────┘

       /    |    \

      ▼     ▼     ▼

     Dog   Cat   Bird

      │     │      │
      ▼     ▼      ▼

    Bark   Meow   Chirp
Polymorphism in Simple Words
  • One name can have multiple forms.
  • One parent reference can represent different child objects.
  • The same method call can produce different behavior.
  • The actual implementation can be selected automatically.
  • Code can depend on abstractions instead of specific classes.

Why Use Polymorphism?

Flexible Code

The same code can work with many different object types.

Extensibility

New implementations can often be added without changing existing code.

Loose Coupling

Code can depend on parent classes, abstract classes, or interfaces.

Reusable Logic

Common processing logic can work with multiple implementations.

Dynamic Behavior

The correct overridden method can be selected at runtime.

Testability

Implementations can be replaced with test versions more easily.

Real-World Analogy

Consider a payment system. A customer can make a payment using a credit card, UPI, or digital wallet. The action is the same—pay—but each payment method performs the operation differently.

Payment Analogy
             PAYMENT

               pay()

          /      |       \

         /       |        \

        ▼        ▼         ▼

 Credit Card    UPI      Wallet

     │           │          │
     ▼           ▼          ▼

 Card Logic   UPI Logic  Wallet Logic

The application can work with the general Payment type while each implementation decides how the payment is processed.

Types of Polymorphism

Polymorphism Types
POLYMORPHISM

├── Compile-Time Polymorphism
│
│   └── Method Overloading
│
└── Runtime Polymorphism

    └── Method Overriding
Two Main Types
  • Compile-time polymorphism is generally achieved through method overloading.
  • Runtime polymorphism is achieved through method overriding and dynamic method dispatch.

Compile-Time Polymorphism

Compile-time polymorphism occurs when the compiler determines which method should be called. Method overloading is the main example.

Compile-Time Selection
class Calculator {

    int add(int a, int b) {

        return a + b;

    }


    double add(
        double a,
        double b
    ) {

        return a + b;

    }

}
Method Calls
Calculator calculator =
        new Calculator();


calculator.add(10, 20);

calculator.add(10.5, 20.5);

The compiler selects the correct add() method by examining the arguments.

Method Overloading

Method overloading allows multiple methods in the same class to have the same name with different parameter lists.

Overloaded Methods
class Printer {

    void print(String text) {

        System.out.println(text);

    }


    void print(int number) {

        System.out.println(number);

    }


    void print(
        String text,
        int copies
    ) {

        for (
            int i = 0;
            i < copies;
            i++
        ) {

            System.out.println(text);

        }

    }

}

Rules of Method Overloading

  • Overloaded methods must have the same method name.
  • The parameter list must be different.
  • The number of parameters can be different.
  • The parameter types can be different.
  • The order of parameter types can be different.
  • Return type alone cannot overload a method.
  • Access modifiers may be different.
  • Methods may throw different exceptions.
  • Static methods can be overloaded.
  • Final methods can be overloaded.
  • Private methods can be overloaded.

Valid Method Overloading

Valid Overloading
void display() {

}


void display(int value) {

}


void display(String value) {

}


void display(
    int first,
    int second
) {

}


void display(
    String text,
    int number
) {

}


void display(
    int number,
    String text
) {

}

Invalid Method Overloading

Return Type Alone is Not Enough
int calculate(int value) {

    return value;

}


double calculate(int value) {

    return value;

}


// Compilation error

Both methods have the same name and the same parameter list. Changing only the return type does not create a different method signature for overloading.

Overloading by Number of Parameters

Different Number of Parameters
class Calculator {

    int add(int a, int b) {

        return a + b;

    }


    int add(
        int a,
        int b,
        int c
    ) {

        return a + b + c;

    }

}

Overloading by Parameter Types

Different Parameter Types
class Converter {

    void convert(int value) {

        System.out.println(
            "Integer: " + value
        );

    }


    void convert(double value) {

        System.out.println(
            "Double: " + value
        );

    }


    void convert(String value) {

        System.out.println(
            "String: " + value
        );

    }

}

Overloading by Parameter Order

Different Parameter Order
void show(
    String name,
    int age
) {

}


void show(
    int age,
    String name
) {

}

Return Type and Overloading

The return type may be different in overloaded methods, but only when the parameter lists are also different.

Valid Different Return Types
int process(int value) {

    return value;

}


double process(double value) {

    return value;

}

Static Method Overloading

Static Overloading
class MathUtility {

    static int max(
        int a,
        int b
    ) {

        return a > b ? a : b;

    }


    static double max(
        double a,
        double b
    ) {

        return a > b ? a : b;

    }

}

Static methods can be overloaded because overloading is resolved using method signatures at compile time.

Constructor Overloading

Constructor Overloading
class Student {

    private String name;

    private int age;


    Student() {

        this(
            "Unknown",
            0
        );

    }


    Student(String name) {

        this(
            name,
            0
        );

    }


    Student(
        String name,
        int age
    ) {

        this.name = name;

        this.age = age;

    }

}

Automatic Type Promotion

When an exact overloaded method is unavailable, Java may promote an argument to a compatible wider type.

Type Promotion
class Demo {

    void show(int value) {

        System.out.println(
            "int method"
        );

    }

}


Demo demo = new Demo();


byte value = 10;


demo.show(value);
Promotion
byte

  │
  ▼

short

  │
  ▼

int

  │
  ▼

long

  │
  ▼

float

  │
  ▼

double

Ambiguous Method Calls

Ambiguous Overloading
class Demo {

    void show(
        int a,
        double b
    ) {

    }


    void show(
        double a,
        int b
    ) {

    }

}


Demo demo = new Demo();


demo.show(10, 10);


// Compilation error

Both overloaded methods are equally valid after type promotion, so the compiler cannot determine which method should be called.

Runtime Polymorphism

Runtime polymorphism occurs when the method implementation is selected while the program is running based on the actual object type.

Runtime Polymorphism
Animal animal =
        new Dog();


animal.sound();
Decision
REFERENCE TYPE

Animal


ACTUAL OBJECT TYPE

Dog


METHOD CALL

animal.sound()


METHOD EXECUTED

Dog.sound()

Method Overriding

Method overriding allows a child class to provide its own implementation of an inherited instance method. It is the foundation of runtime polymorphism.

Method Overriding
class Animal {

    void sound() {

        System.out.println(
            "Animal sound"
        );

    }

}


class Dog extends Animal {

    @Override
    void sound() {

        System.out.println(
            "Dog barks"
        );

    }

}

Runtime Polymorphism Example

Animal.java
public class Animal {

    public void sound() {

        System.out.println(
            "Animal makes a sound"
        );

    }

}
Dog.java
public class Dog
        extends Animal {

    @Override
    public void sound() {

        System.out.println(
            "Dog barks"
        );

    }

}
Cat.java
public class Cat
        extends Animal {

    @Override
    public void sound() {

        System.out.println(
            "Cat meows"
        );

    }

}
Main.java
public class Main {

    public static void main(
        String[] args
    ) {

        Animal animal;


        animal = new Dog();

        animal.sound();


        animal = new Cat();

        animal.sound();

    }

}
Output
Dog barks

Cat meows

Reference Type vs Object Type

Two Different Types
Animal animal =
        new Dog();
Understanding the Statement
Animal animal = new Dog();

   │                 │
   ▼                 ▼

Reference Type    Object Type


COMPILE-TIME CHECKING

Uses Reference Type


OVERRIDDEN METHOD EXECUTION

Uses Actual Object Type
Important Rule
  • The reference type controls which members can be accessed.
  • The actual object type controls which overridden instance method executes.
  • These two rules are essential for understanding runtime polymorphism.

Dynamic Method Dispatch

Dynamic method dispatch is the mechanism by which Java selects the overridden instance method to execute based on the actual object type at runtime.

Dynamic Dispatch
Animal animal;


animal = new Dog();

animal.sound();


animal = new Cat();

animal.sound();
Dispatch Process
animal.sound()

       │
       ▼

Check Actual Object

       │
       ├── Dog Object
       │
       └── Execute Dog.sound()


animal.sound()

       │
       ▼

Check Actual Object

       │
       ├── Cat Object
       │
       └── Execute Cat.sound()

How Runtime Polymorphism Works

Runtime Method Selection
Animal animal = new Dog();

        │
        ▼

Compiler Checks

Does Animal declare or inherit sound()?

        │
        ▼

YES

Code Compiles

        │
        ▼

Program Runs

        │
        ▼

Actual Object is Dog

        │
        ▼

Dog overrides sound()?

        │
        ▼

YES

        │
        ▼

Execute Dog.sound()

Upcasting and Polymorphism

Upcasting stores a child object in a parent-type reference. This is the most common way to achieve runtime polymorphism.

Upcasting
Dog dog = new Dog();


Animal animal = dog;
Direct Upcasting
Animal animal =
        new Dog();
Why Upcasting Matters
  • One parent type can represent many child types.
  • Methods can accept general parent types.
  • Collections can store different child implementations.
  • Runtime method dispatch becomes possible.
  • Code becomes less dependent on concrete classes.

Parent Reference Limitations

Child-Specific Method
class Dog extends Animal {

    @Override
    void sound() {

        System.out.println(
            "Dog barks"
        );

    }


    void fetch() {

        System.out.println(
            "Dog fetches"
        );

    }

}
Reference Limitation
Animal animal =
        new Dog();


animal.sound();


// Not allowed

animal.fetch();

The actual object is a Dog, but the reference type is Animal. Only members available through Animal can be accessed directly.

Accessing Child-Specific Members

Using a Child Reference
Dog dog = new Dog();


dog.sound();

dog.fetch();
Using Downcasting
Animal animal =
        new Dog();


Dog dog =
        (Dog) animal;


dog.fetch();

Downcasting

Downcasting converts a parent-type reference to a more specific child-type reference. It requires an explicit cast.

Valid Downcasting
Animal animal =
        new Dog();


Dog dog =
        (Dog) animal;
Invalid Downcasting
Animal animal =
        new Cat();


Dog dog =
        (Dog) animal;


// ClassCastException

instanceof Operator

Safe Downcasting
if (animal instanceof Dog) {

    Dog dog =
            (Dog) animal;


    dog.fetch();

}

The instanceof operator checks whether an object is compatible with a particular type before casting.

Pattern Matching with instanceof

Pattern Matching
if (animal instanceof Dog dog) {

    dog.fetch();

}

Pattern matching combines the type check and creation of the correctly typed variable.

Polymorphic Arrays

Array of Parent References
Animal[] animals = {

    new Dog(),

    new Cat(),

    new Bird()

};
Processing Different Objects
for (Animal animal : animals) {

    animal.sound();

}
Output
Dog barks

Cat meows

Bird chirps

Polymorphic Method Parameters

General Method Parameter
public static void makeSound(
    Animal animal
) {

    animal.sound();

}
Different Arguments
makeSound(
    new Dog()
);


makeSound(
    new Cat()
);


makeSound(
    new Bird()
);
Major Benefit
  • One method works with many object types.
  • No separate method is required for each child class.
  • New child classes can often work without changing the method.
  • The correct overridden behavior is selected automatically.

Polymorphic Return Types

Returning a Parent Type
static Animal createAnimal(
    String type
) {

    if (type.equals("dog")) {

        return new Dog();

    }


    return new Cat();

}
Using the Result
Animal animal =
        createAnimal("dog");


animal.sound();

Covariant Return Types

An overriding method can return a more specific reference type than the parent method when the return types are related through inheritance.

Covariant Return Type
class AnimalFactory {

    Animal create() {

        return new Animal();

    }

}


class DogFactory
        extends AnimalFactory {

    @Override
    Dog create() {

        return new Dog();

    }

}

Polymorphism with Abstract Classes

Abstract Parent
abstract class Shape {

    abstract double area();


    void displayArea() {

        System.out.println(
            "Area: " + area()
        );

    }

}
Circle Implementation
class Circle extends Shape {

    private final double radius;


    Circle(double radius) {

        this.radius = radius;

    }


    @Override
    double area() {

        return Math.PI
                * radius
                * radius;

    }

}
Rectangle Implementation
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;

    }

}
Polymorphic Processing
Shape[] shapes = {

    new Circle(5),

    new Rectangle(10, 5)

};


for (Shape shape : shapes) {

    shape.displayArea();

}

Polymorphism with Interfaces

Payment Interface
interface Payment {

    void pay(double amount);

}
CreditCardPayment.java
class CreditCardPayment
        implements Payment {

    @Override
    public void pay(double amount) {

        System.out.println(
            "Paid ₹"
            + amount
            + " using Credit Card"
        );

    }

}
UPIPayment.java
class UPIPayment
        implements Payment {

    @Override
    public void pay(double amount) {

        System.out.println(
            "Paid ₹"
            + amount
            + " using UPI"
        );

    }

}
Using Interface Polymorphism
Payment payment;


payment =
        new CreditCardPayment();

payment.pay(5000);


payment =
        new UPIPayment();

payment.pay(2500);

Multiple Implementations

One Interface, Many Implementations
             PAYMENT

               pay()

         /       |        \

        /        |         \

       ▼         ▼          ▼

 CreditCard     UPI       Wallet

       │         │          │
       ▼         ▼          ▼

 Different   Different   Different
  Logic       Logic       Logic

The calling code can depend on Payment instead of depending directly on CreditCardPayment, UPIPayment, or WalletPayment.

Polymorphism and Dependency Injection

Depending on an Abstraction
class OrderService {

    private final Payment payment;


    OrderService(Payment payment) {

        this.payment = payment;

    }


    void checkout(double amount) {

        payment.pay(amount);

    }

}
Injecting Different Implementations
OrderService cardOrder =
        new OrderService(
            new CreditCardPayment()
        );


OrderService upiOrder =
        new OrderService(
            new UPIPayment()
        );
Why This Design is Flexible
  • OrderService depends on Payment rather than a specific payment class.
  • The implementation can be changed without modifying OrderService.
  • New payment methods can be added more easily.
  • Testing becomes easier because test implementations can be injected.
  • This principle is widely used in frameworks such as Spring.

Static Methods and Polymorphism

Static methods do not participate in runtime polymorphism. They belong to classes and are resolved using the reference type.

Static Method Hiding
class Parent {

    static void display() {

        System.out.println(
            "Parent"
        );

    }

}


class Child extends Parent {

    static void display() {

        System.out.println(
            "Child"
        );

    }

}
Reference Type Controls Static Method
Parent reference =
        new Child();


reference.display();
Output
Parent

Fields and Polymorphism

Fields are not polymorphic. Field access is determined by the reference type.

Field Hiding
class Parent {

    String name = "Parent";

}


class Child extends Parent {

    String name = "Child";

}
Field Access
Parent reference =
        new Child();


System.out.println(
    reference.name
);
Output
Parent

Private Methods

Private methods are not inherited in a way that allows overriding. A child method with the same name is a separate method.

Private Methods Are Not Overridden
class Parent {

    private void display() {

        System.out.println(
            "Parent"
        );

    }

}


class Child extends Parent {

    void display() {

        System.out.println(
            "Child"
        );

    }

}

Final Methods

Final Method
class Parent {

    final void display() {

        System.out.println(
            "Fixed behavior"
        );

    }

}

A final method cannot be overridden, so its behavior cannot participate in runtime polymorphism through overriding.

Method Overloading vs Method Overriding

Comparison
METHOD OVERLOADING

Same method name

Different parameter list

Usually same class

Compile-time decision

Inheritance not required

Static methods can be overloaded



METHOD OVERRIDING

Same method signature

Parent-child relationship

Runtime decision

Inheritance required

Static methods are not overridden

Early Binding vs Late Binding

Binding Comparison
EARLY BINDING

Decision made at compile time

Used with:

Method overloading
Static methods
Private methods
Final methods



LATE BINDING

Decision made at runtime

Used with:

Overridden instance methods

Dynamic method dispatch

Compile-Time vs Runtime Polymorphism

Complete Comparison
COMPILE-TIME POLYMORPHISM

Achieved through method overloading

Decision made by compiler

Based on method signature

Also called static polymorphism

Generally faster method selection



RUNTIME POLYMORPHISM

Achieved through method overriding

Decision made while program runs

Based on actual object type

Also called dynamic polymorphism

Supports flexible object-oriented design

Payment Example

Payment.java
public interface Payment {

    boolean pay(double amount);

}
CreditCardPayment.java
public class CreditCardPayment
        implements Payment {

    @Override
    public boolean pay(double amount) {

        if (amount <= 0) {

            return false;

        }


        System.out.println(
            "Credit card payment: ₹"
            + amount
        );


        return true;

    }

}
UPIPayment.java
public class UPIPayment
        implements Payment {

    @Override
    public boolean pay(double amount) {

        if (amount <= 0) {

            return false;

        }


        System.out.println(
            "UPI payment: ₹"
            + amount
        );


        return true;

    }

}
CheckoutService.java
public class CheckoutService {

    private final Payment payment;


    public CheckoutService(
        Payment payment
    ) {

        this.payment = payment;

    }


    public void checkout(
        double amount
    ) {

        if (payment.pay(amount)) {

            System.out.println(
                "Order completed"
            );

        }

    }

}

Employee Example

Employee.java
public abstract class Employee {

    private final String name;


    public Employee(String name) {

        this.name = name;

    }


    public String getName() {

        return name;

    }


    public abstract double
        calculateSalary();

}
FullTimeEmployee.java
public class FullTimeEmployee
        extends Employee {

    private final double
        monthlySalary;


    public FullTimeEmployee(
        String name,
        double monthlySalary
    ) {

        super(name);

        this.monthlySalary =
                monthlySalary;

    }


    @Override
    public double calculateSalary() {

        return monthlySalary;

    }

}
ContractEmployee.java
public class ContractEmployee
        extends Employee {

    private final double hourlyRate;

    private final int hoursWorked;


    public ContractEmployee(
        String name,
        double hourlyRate,
        int hoursWorked
    ) {

        super(name);

        this.hourlyRate = hourlyRate;

        this.hoursWorked =
                hoursWorked;

    }


    @Override
    public double calculateSalary() {

        return hourlyRate
                * hoursWorked;

    }

}
Payroll Processing
Employee[] employees = {

    new FullTimeEmployee(
        "Aarav",
        60000
    ),

    new ContractEmployee(
        "Diya",
        500,
        80
    )

};


for (Employee employee : employees) {

    System.out.println(
        employee.getName()
        + ": ₹"
        + employee.calculateSalary()
    );

}

Notification Example

NotificationService.java
public interface NotificationService {

    void send(
        String recipient,
        String message
    );

}
EmailNotification.java
public class EmailNotification
        implements NotificationService {

    @Override
    public void send(
        String recipient,
        String message
    ) {

        System.out.println(
            "Email sent to "
            + recipient
        );

    }

}
SMSNotification.java
public class SMSNotification
        implements NotificationService {

    @Override
    public void send(
        String recipient,
        String message
    ) {

        System.out.println(
            "SMS sent to "
            + recipient
        );

    }

}
NotificationManager.java
public class NotificationManager {

    private final NotificationService
        notificationService;


    public NotificationManager(
        NotificationService
            notificationService
    ) {

        this.notificationService =
                notificationService;

    }


    public void notifyUser(
        String recipient,
        String message
    ) {

        notificationService.send(
            recipient,
            message
        );

    }

}

Shape Example

Shape.java
public abstract class Shape {

    public abstract double area();

}
Circle.java
public class Circle
        extends Shape {

    private final double radius;


    public Circle(double radius) {

        this.radius = radius;

    }


    @Override
    public double area() {

        return Math.PI
                * radius
                * radius;

    }

}
Rectangle.java
public class Rectangle
        extends Shape {

    private final double width;

    private final double height;


    public Rectangle(
        double width,
        double height
    ) {

        this.width = width;

        this.height = height;

    }


    @Override
    public double area() {

        return width * height;

    }

}
Area Calculator
public static double totalArea(
    Shape[] shapes
) {

    double total = 0;


    for (Shape shape : shapes) {

        total += shape.area();

    }


    return total;

}

Bank Account Example

BankAccount.java
public abstract class BankAccount {

    private double balance;


    public BankAccount(
        double balance
    ) {

        this.balance = balance;

    }


    public double getBalance() {

        return balance;

    }


    public abstract double
        calculateInterest();

}
SavingsAccount.java
public class SavingsAccount
        extends BankAccount {

    public SavingsAccount(
        double balance
    ) {

        super(balance);

    }


    @Override
    public double calculateInterest() {

        return getBalance() * 0.04;

    }

}
FixedDepositAccount.java
public class FixedDepositAccount
        extends BankAccount {

    public FixedDepositAccount(
        double balance
    ) {

        super(balance);

    }


    @Override
    public double calculateInterest() {

        return getBalance() * 0.07;

    }

}

Common Polymorphism Errors

Common Errors
POLYMORPHISM ERRORS

├── Confusing Overloading and Overriding
├── Changing Only Return Type for Overloading
├── Creating Ambiguous Overloaded Methods
├── Ignoring Automatic Type Promotion
├── Assuming Overloading Happens at Runtime
├── Using Wrong Parameter Types
├── Forgetting @Override
├── Changing Method Signature Accidentally
├── Reducing Visibility While Overriding
├── Trying to Override final Methods
├── Trying to Override Private Methods
├── Treating Static Methods as Polymorphic
├── Confusing Static Hiding with Overriding
├── Treating Fields as Polymorphic
├── Confusing Reference Type with Object Type
├── Calling Child-Specific Methods Through Parent Reference
├── Performing Unsafe Downcasting
├── Ignoring ClassCastException
├── Using instanceof Everywhere
├── Excessive Type Checking
├── Using Downcasting Instead of Polymorphism
├── Depending on Concrete Classes
├── Creating Separate Logic for Every Child Type
├── Using if-else Chains Instead of Overriding
├── Using switch Statements for Object Types
├── Breaking Parent Method Contracts
├── Returning Incompatible Types
├── Misusing Covariant Return Types
├── Forgetting Runtime Dispatch Applies to Instance Methods
├── Assuming Constructors are Polymorphic
├── Calling Overridable Methods from Constructors
├── Creating Overly Complex Hierarchies
├── Using Inheritance Without IS-A Relationship
├── Ignoring Interfaces
├── Ignoring Abstract Classes
├── Overusing Method Overloading
├── Creating Too Many Similar Overloads
├── Using null with Ambiguous Overloads
├── Forgetting Boxing and Unboxing Effects
├── Forgetting Varargs Can Affect Overload Resolution
├── Assuming Parent Reference Changes the Object
├── Assuming Upcasting Creates a New Object
├── Assuming Downcasting Changes the Object
├── Mixing Field Hiding with Method Overriding
├── Depending on Reference Type for Overridden Methods
├── Depending on Object Type for Fields
├── Tight Coupling to Implementations
├── Violating Substitution Expectations
├── Adding Child Behavior That Breaks Parent Contracts
├── Poor Interface Design
├── God Interfaces
├── Excessive Downcasting
└── Missing the Main Purpose of Polymorphism

Best Practices

  • Program to abstractions instead of concrete implementations.
  • Use parent classes, abstract classes, and interfaces as reference types.
  • Use runtime polymorphism to remove unnecessary type-based conditions.
  • Use @Override whenever overriding a method.
  • Keep overridden behavior compatible with parent expectations.
  • Do not reduce method visibility when overriding.
  • Use meaningful method overloading.
  • Avoid excessive overloads that make APIs confusing.
  • Avoid ambiguous overloaded method signatures.
  • Do not rely only on return types for overloading.
  • Understand automatic type promotion.
  • Understand boxing, unboxing, and varargs when designing overloads.
  • Use upcasting naturally.
  • Avoid unnecessary downcasting.
  • Use instanceof only when type-specific behavior is genuinely required.
  • Prefer polymorphic method calls over repeated type checks.
  • Use interfaces when unrelated classes share a common capability.
  • Use abstract classes when implementations share state and behavior.
  • Keep interfaces focused.
  • Keep abstractions stable.
  • Inject implementations instead of creating them inside dependent classes.
  • Use constructor injection for required dependencies.
  • Use polymorphic arrays and collections for related object types.
  • Accept general types in method parameters.
  • Return abstractions when callers do not need concrete implementation details.
  • Use covariant return types when a more specific result improves the API.
  • Remember that static methods are not runtime polymorphic.
  • Remember that fields are not runtime polymorphic.
  • Remember that private methods are not overridden.
  • Remember that final methods cannot be overridden.
  • Keep inheritance hierarchies shallow.
  • Use genuine IS-A relationships.
  • Prefer composition when behavior should be replaceable dynamically.
  • Design child classes as valid substitutes for parent classes.
  • Do not break parent contracts in overridden methods.
  • Avoid throwing unsupported-operation exceptions for expected parent behavior.
  • Use polymorphism to make systems extensible.
  • Add new implementations without modifying stable calling code when possible.
  • Test implementations through their abstraction.
  • Use test doubles when appropriate.
  • Keep business logic independent of implementation details.
  • Avoid large if-else chains based on object type.
  • Avoid switch statements that repeatedly inspect implementation types.
  • Let objects perform their own specialized behavior.
  • Use descriptive abstraction names.
  • Keep methods cohesive.
  • Document important contracts.
  • Test runtime dispatch behavior.
  • Understand reference type and object type separately.
  • Use polymorphism as a design principle, not merely as syntax.

Common Misconceptions

Avoid These Misconceptions
  • Polymorphism does not mean only method overriding.
  • Java supports both compile-time and runtime polymorphism.
  • Method overloading is resolved at compile time.
  • Method overriding is resolved at runtime for eligible instance methods.
  • Return type alone cannot overload a method.
  • Overloaded methods do not require inheritance.
  • Overridden methods require an inheritance or implementation relationship.
  • Static methods are not runtime polymorphic.
  • Fields are not runtime polymorphic.
  • Private methods are not overridden.
  • Final methods cannot be overridden.
  • Constructors are not overridden.
  • A parent reference can refer to a child object.
  • A child reference cannot directly refer to a general parent object.
  • Upcasting does not create a new object.
  • Downcasting does not change the actual object.
  • The reference type controls accessible members.
  • The actual object type controls overridden method execution.
  • A parent reference cannot directly call child-specific methods.
  • Downcasting can fail at runtime.
  • instanceof should not replace good polymorphic design.
  • Runtime polymorphism requires eligible overridden instance methods.
  • Method overloading is not dynamic method dispatch.
  • The same object can be referenced through different compatible types.
  • Interfaces are major tools for polymorphic design.
  • Abstract classes also support polymorphism.
  • Polymorphism reduces dependency on concrete classes.
  • Polymorphism does not automatically guarantee good design.
  • Too much downcasting often indicates a weak abstraction.
  • Repeated type checking may indicate missing polymorphic behavior.

Practice Exercises

Exercise 1: Calculator Overloading
  • Create a Calculator class.
  • Overload add() for two integers.
  • Overload add() for three integers.
  • Overload add() for two doubles.
  • Test all methods.
Exercise 2: Animal Polymorphism
  • Create an Animal parent class.
  • Create Dog, Cat, and Bird child classes.
  • Override sound().
  • Store all objects in an Animal array.
  • Call sound() using a loop.
Exercise 3: Payment System
  • Create a Payment interface.
  • Create CreditCardPayment.
  • Create UPIPayment.
  • Create WalletPayment.
  • Process all payments through Payment references.
Exercise 4: Shape Calculator
  • Create an abstract Shape class.
  • Create Circle and Rectangle.
  • Override area().
  • Store shapes in an array.
  • Calculate total area polymorphically.
Exercise 5: Employee Payroll
  • Create an abstract Employee class.
  • Create FullTimeEmployee.
  • Create ContractEmployee.
  • Override calculateSalary().
  • Process payroll through Employee references.
Exercise 6: Notification System
  • Create a NotificationService interface.
  • Create EmailNotification.
  • Create SMSNotification.
  • Create PushNotification.
  • Inject implementations into a NotificationManager.
Exercise 7: Safe Downcasting
  • Create Animal and Dog classes.
  • Store a Dog in an Animal reference.
  • Check the type using instanceof.
  • Use pattern matching.
  • Call a Dog-specific method safely.
Exercise 8: Remove Type Conditions
  • Create code that uses if-else based on object type.
  • Identify the changing behavior.
  • Move behavior into overridden methods.
  • Use a parent type or interface.
  • Compare both designs.

Common Interview Questions

What is polymorphism in Java?

Polymorphism allows one method name, reference type, abstract class, or interface to represent multiple forms of behavior.

What are the types of polymorphism in Java?

The two main types are compile-time polymorphism and runtime polymorphism.

How is compile-time polymorphism achieved?

It is generally achieved through method overloading.

How is runtime polymorphism achieved?

It is achieved through method overriding and dynamic method dispatch.

What is method overloading?

Method overloading allows methods with the same name to have different parameter lists.

Can return type alone overload a method?

No.

What is method overriding?

Method overriding occurs when a child class provides its own implementation of an inherited instance method.

What is dynamic method dispatch?

It is the runtime mechanism that selects an overridden method based on the actual object type.

What is upcasting?

Upcasting stores a child object in a parent-type reference.

What controls accessible members?

The reference type controls which members can be accessed at compile time.

What controls overridden method execution?

The actual object type controls which overridden instance method executes.

Are static methods polymorphic?

Static methods do not participate in runtime polymorphism. They are hidden rather than overridden.

Are fields polymorphic?

No. Field access is based on the reference type.

What is a covariant return type?

It allows an overriding method to return a more specific reference type than the parent method.

What is the difference between early and late binding?

Early binding resolves a call at compile time, while late binding resolves an overridden instance method at runtime.

Frequently Asked Questions

Can one parent reference point to different child objects?

Yes. This is a central use of runtime polymorphism.

Can a parent reference call child-specific methods?

Not directly. The member must be available through the reference type, or safe downcasting must be used.

Does upcasting create a new object?

No. It creates another compatible reference to the same object.

Does downcasting change the object?

No. It only changes the reference type used to access the existing object.

Should I use instanceof frequently?

Usually no. Frequent type checks may indicate that behavior should be handled polymorphically.

Are interfaces better for polymorphism?

Interfaces are excellent for capability-based polymorphism and loose coupling, while abstract classes are useful when implementations also share state or common behavior.

What should I learn after Polymorphism?

The next lesson covers Abstraction in Java.

Key Takeaways

  • Polymorphism means many forms.
  • Polymorphism is a fundamental Object-Oriented Programming principle.
  • The same method call can produce different behavior.
  • One parent reference can represent different child objects.
  • Java supports compile-time and runtime polymorphism.
  • Compile-time polymorphism is generally achieved through method overloading.
  • Runtime polymorphism is achieved through method overriding.
  • Method overloading uses the same method name with different parameter lists.
  • Overloading can differ by parameter count.
  • Overloading can differ by parameter types.
  • Overloading can differ by parameter order.
  • Return type alone cannot overload a method.
  • Static methods can be overloaded.
  • Constructors can be overloaded.
  • Automatic type promotion can affect overload selection.
  • Ambiguous overloaded calls cause compilation errors.
  • Method overriding requires an inheritance or implementation relationship.
  • The @Override annotation should be used.
  • Dynamic method dispatch selects overridden methods at runtime.
  • The reference type controls accessible members.
  • The actual object type controls overridden method execution.
  • Upcasting enables runtime polymorphism.
  • Upcasting is automatic and safe.
  • A parent reference cannot directly access child-specific members.
  • Downcasting requires an explicit cast.
  • Invalid downcasting can cause ClassCastException.
  • instanceof can check type compatibility.
  • Pattern matching simplifies instanceof usage.
  • Polymorphic arrays can store different child objects.
  • Polymorphic method parameters allow one method to process many implementations.
  • Polymorphic return types allow methods to return different implementations.
  • Covariant return types allow more specific return types in overriding methods.
  • Abstract classes support runtime polymorphism.
  • Interfaces strongly support flexible polymorphic design.
  • One interface can have many implementations.
  • Dependency injection relies heavily on polymorphism.
  • Static methods do not provide runtime polymorphism.
  • Fields are not polymorphic.
  • Private methods are not overridden.
  • Final methods cannot be overridden.
  • Overloading is generally compile-time behavior.
  • Overriding provides runtime behavior.
  • Early binding occurs at compile time.
  • Late binding occurs at runtime.
  • Polymorphism reduces dependency on concrete classes.
  • Polymorphism supports loose coupling.
  • Polymorphism improves extensibility.
  • New implementations can often be added without changing calling code.
  • Repeated type checks can often be replaced with polymorphism.
  • Excessive downcasting may indicate a poor abstraction.
  • Good polymorphic design depends on stable abstractions.
  • Child implementations should preserve parent contracts.
  • Polymorphism is widely used in real applications and frameworks.

Summary

Polymorphism means many forms. It allows one method name, parent type, abstract class, or interface to represent different implementations and behaviors.

Java supports compile-time polymorphism and runtime polymorphism. Compile-time polymorphism is generally achieved through method overloading, while runtime polymorphism is achieved through method overriding and dynamic method dispatch.

Method overloading allows multiple methods to use the same name with different parameter lists. The compiler determines which overloaded method should execute by examining the method arguments.

Runtime polymorphism allows a parent reference to refer to different child objects. The reference type controls which members are accessible, while the actual object type determines which overridden instance method executes.

Upcasting is central to runtime polymorphism because it allows child objects to be handled through general parent, abstract-class, or interface references. Downcasting should be used only when child-specific behavior is genuinely required.

Abstract classes and interfaces allow multiple implementations to be processed through common abstractions. This reduces coupling and makes applications easier to extend, test, and maintain.

Static methods and fields do not participate in runtime polymorphism. Static method calls and field access depend on the reference or class type, while overridden instance methods depend on the actual object type.

Good polymorphic design depends on meaningful abstractions, compatible implementations, limited downcasting, and code that delegates specialized behavior to objects instead of repeatedly checking their concrete types.

Lesson 23 Completed
  • You understand what polymorphism is.
  • You understand why polymorphism is useful.
  • You know the types of polymorphism.
  • You understand compile-time polymorphism.
  • You can create overloaded methods.
  • You know method overloading rules.
  • You understand valid and invalid overloading.
  • You understand automatic type promotion.
  • You understand ambiguous method calls.
  • You understand runtime polymorphism.
  • You understand method overriding.
  • You understand reference type and object type.
  • You understand dynamic method dispatch.
  • You understand how runtime method selection works.
  • You can use upcasting.
  • You understand parent reference limitations.
  • You can safely use downcasting.
  • You can use instanceof.
  • You understand pattern matching with instanceof.
  • You can create polymorphic arrays.
  • You can create polymorphic method parameters.
  • You understand polymorphic return types.
  • You understand covariant return types.
  • You understand polymorphism with abstract classes.
  • You understand polymorphism with interfaces.
  • You understand multiple implementations.
  • You understand polymorphism and dependency injection.
  • You understand why static methods are not runtime polymorphic.
  • You understand why fields are not polymorphic.
  • You know the difference between overloading and overriding.
  • You understand early and late binding.
  • You can design flexible polymorphic systems.
  • You can identify common polymorphism errors.
  • You know polymorphism best practices.
  • You are ready to learn Abstraction in Java.
Next Lesson →

Abstraction in Java