ZeroToSky
Java Tutorial 39 of 143
Basics
  • Introduction
  • Download and Install Java
  • Download and Install IntelliJ IDEA, Eclipse
  • Creating First Java Application in IntelliJ IDEA
  • JDK vs JRE vs JVM
  • Print output
  • Taking Input
  • Identifiers
  • Keywords
  • Variables
  • Data Types
  • Literals
  • Wrapper Classes
  • Operators
  • Decision Making
  • Loops and Jump Statements
  • Project: Number Guessing Game

All 17 lessons →

Methods
  • Introduction
  • Static Methods vs Instance Methods
  • Access Modifiers
  • Command Line Arguments
  • Variable Arguments (Varargs)

All 5 lessons →

Arrays
  • Introduction
  • One Dimensional Arrays
  • Multidimensional Arrays
  • Array Class
  • Arrays Class

All 5 lessons →

Strings
  • Introduction
  • String
  • StringBuffer
  • StringBuilder
  • String Class Methods
  • StringJoiner
  • StringTokenizer

All 7 lessons →

OOP & Interface

All 14 lessons →

Collections
  • Collection class
  • Collection Interface
  • List Interface
  • ArrayList
  • LinkedList
  • Set Interface
  • HashSet
  • LinkedHashSet
  • TreeSet
  • Queue Interface
  • PriorityQueue
  • Deque
  • ArrayDeque
  • Map Interface in Java
  • HashMap
  • LinkedHashMap
  • TreeMap
  • Hashtable
  • Iterator
  • Comparable and Comparator

All 20 lessons →

Exception Handling

All 7 lessons →

Multithreading & Concurrency
  • Introduction
  • Thread Class
  • Runnable Interface
  • Thread Life Cycle
  • Thread Priority
  • Synchronization
  • Inter-Thread Communication
  • Executor Framework
  • Callable and Future
  • Concurrent Collections

All 10 lessons →

Java 8 Features
  • Lambda Expressions
  • Functional Interfaces
  • Method References
  • Stream API
  • Optional Class
  • Default and Static Methods in Interface
  • Date and Time API

All 7 lessons →

Advanced Java Concepts
  • File Handling
  • Serialization and Deserialization
  • Reflection API
  • Annotations
  • Garbage Collection
  • Memory Management
  • JVM Architecture
  • Class Loading
  • Networking
  • Regular Expressions

All 10 lessons →

JDBC
  • Introduction
  • JDBC Driver
  • JDBC Connection
  • Types of Statements in JDBC
  • Transactions
  • ResultSet
  • Metadata
  • Connection Pooling

All 8 lessons →

GUI & Desktop
  • AWT
  • Swing
  • JavaFX

All 3 lessons →

Web & Enterprise
  • Servlets
  • JSP
  • Spring Framework
  • Spring Boot
  • Hibernate
  • REST API Development

All 6 lessons →

Practice Projects
  • Project: Number Guessing Game
  • Project: Banking System
  • Project: Student Management System
  • Project: Library Management System
  • Project: Quiz Application

All 5 lessons →

Quiz and Coding Practice
  • Java Topic-wise Quiz
  • Coding Practice Problems
  • Java Basics Coding Practice Problems
  • Java Exercises

All 4 lessons →

Interview Questions
  • Core Java Interview Questions and Answers
  • Advanced Java Interview Questions and Answers
  • Java Interview Questions

All 3 lessons →

DSA & Competitive Programming
  • Arrays and Strings
  • Linked List
  • Stack
  • Queue
  • Tree
  • Graph
  • Heap
  • Hashing
  • Sorting
  • Searching
  • Dynamic Programming
  • Roadmap for DSA in Java

All 12 lessons →

Java Polymorphism

Learn Java polymorphism, including compile-time and runtime polymorphism, method overloading, method overriding, dynamic method dispatch, and practical examples.

Gaurav 7 min read Sign in to save
On this page

Java Polymorphism

Polymorphism is one of the four fundamental concepts of object-oriented programming (OOP) in Java. The word polymorphism means "many forms."

In Java, polymorphism allows the same method call or interface to behave differently depending on the context or the actual object involved.

For example, different animals can implement the same sound() method differently:

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");
    }
}

The same method call:

animal.sound();

can produce different behavior depending on the object.

What Is Polymorphism in Java?

Polymorphism allows a single interface, method name, or reference type to represent different behaviors.

For example:

Animal animal = new Dog();

animal.sound();

Output:

Dog barks

The reference type is Animal, but the actual object is Dog. At runtime, Java invokes the overridden Dog implementation.

This is called runtime polymorphism.

Types of Polymorphism in Java

Polymorphism in Java is commonly divided into two types:

  1. Compile-time polymorphism
  2. Runtime polymorphism

Compile-time polymorphism is commonly achieved through method overloading.

Runtime polymorphism is commonly achieved through method overriding and inheritance.


Compile-Time Polymorphism

Compile-time polymorphism occurs when the compiler determines which method should be called during compilation.

The most common example is method overloading.

Method Overloading

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

Example:

class Calculator {

    int add(int a, int b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }

    double add(double a, double b) {
        return a + b;
    }
}

The method name is the same:

add

but the parameter lists are different.

Usage:

Calculator calculator = new Calculator();

System.out.println(calculator.add(10, 20));
System.out.println(calculator.add(10, 20, 30));
System.out.println(calculator.add(10.5, 20.5));

Output:

30
60
31.0

The compiler determines which overloaded method matches the arguments.

Rules for Method Overloading

Methods can be overloaded by changing:

  • Number of parameters
  • Type of parameters
  • Order of parameters when their types differ

For example:

void display(int value) {
}

void display(String value) {
}

void display(int value, String text) {
}

void display(String text, int value) {
}

However, changing only the return type is not enough.

This is invalid:

int calculate(int a) {
    return a;
}

double calculate(int a) {
    return a;
}

The parameter list is identical, so these methods cannot be overloaded based only on return type.


Runtime Polymorphism

Runtime polymorphism occurs when the method implementation is selected at runtime based on the actual object.

It is commonly achieved through method overriding.

Example:

class Animal {

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

class Dog extends Animal {

    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}

Now:

Animal animal = new Dog();

animal.sound();

Output:

Dog barks

Although the reference variable has type Animal, the object is a Dog.

Therefore, Java executes the overridden Dog.sound() method.

Dynamic Method Dispatch

The mechanism through which Java determines the overridden method at runtime is called dynamic method dispatch.

Example:

class Animal {

    void sound() {
        System.out.println("Animal 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");
    }
}

Now:

Animal animal;

animal = new Dog();
animal.sound();

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

Output:

Dog barks
Cat meows

The same reference type can refer to different objects, and the appropriate overridden method is selected at runtime.


Method Overriding

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

Example:

class Vehicle {

    void start() {
        System.out.println("Vehicle starts");
    }
}

class Car extends Vehicle {

    @Override
    void start() {
        System.out.println("Car starts with a button");
    }
}

Now:

Vehicle vehicle = new Car();

vehicle.start();

Output:

Car starts with a button

The child implementation overrides the parent implementation.

Rules for Method Overriding

Some important rules include:

  • The child method must have the same method signature as the inherited method.
  • The return type must be compatible with the parent method's return type.
  • The overriding method cannot reduce access visibility.
  • A final method cannot be overridden.
  • A static method is hidden rather than overridden.
  • Private methods are not overridden because they are not inherited by subclasses.
  • The @Override annotation is recommended.

Example:

class Parent {

    protected void display() {
        System.out.println("Parent");
    }
}

class Child extends Parent {

    @Override
    public void display() {
        System.out.println("Child");
    }
}

The child method can increase visibility from protected to public, but it cannot reduce it to private.


Polymorphism with Parent References

One of the most important uses of runtime polymorphism is assigning a child object to a parent reference.

Animal animal = new Dog();

This is valid because Dog is an Animal.

The reference can invoke methods available through the Animal type:

animal.sound();

If Dog overrides sound(), the Dog implementation runs.

This allows code to work with the parent abstraction while supporting different implementations.


Polymorphism with Multiple Subclasses

Consider a payment system:

class Payment {

    void pay() {
        System.out.println("Processing payment");
    }
}

class CreditCardPayment extends Payment {

    @Override
    void pay() {
        System.out.println("Processing credit card payment");
    }
}

class UpiPayment extends Payment {

    @Override
    void pay() {
        System.out.println("Processing UPI payment");
    }
}

The same parent reference can represent different payment types:

Payment payment;

payment = new CreditCardPayment();
payment.pay();

payment = new UpiPayment();
payment.pay();

Output:

Processing credit card payment
Processing UPI payment

The calling code does not need to know the specific implementation to invoke pay().


Polymorphism with Interfaces

Polymorphism is also commonly used with interfaces.

Consider:

interface Payment {

    void pay(double amount);
}

Different classes can implement the interface:

class CreditCardPayment implements Payment {

    @Override
    public void pay(double amount) {
        System.out.println("Paid using credit card: " + amount);
    }
}
class UpiPayment implements Payment {

    @Override
    public void pay(double amount) {
        System.out.println("Paid using UPI: " + amount);
    }
}

Now:

Payment payment = new UpiPayment();

payment.pay(1000);

The code depends on the Payment interface rather than a specific implementation.

This is an important technique for creating loosely coupled applications.


Polymorphism with Abstract Classes

Abstract classes can also be used to achieve runtime polymorphism.

Example:

abstract class Shape {

    abstract void draw();
}

Different shapes can provide different implementations:

class Circle extends Shape {

    @Override
    void draw() {
        System.out.println("Drawing circle");
    }
}
class Rectangle extends Shape {

    @Override
    void draw() {
        System.out.println("Drawing rectangle");
    }
}

Now:

Shape shape;

shape = new Circle();
shape.draw();

shape = new Rectangle();
shape.draw();

Output:

Drawing circle
Drawing rectangle

The parent abstraction provides the common contract while each subclass supplies its behavior.


Upcasting

Upcasting occurs when a child object is assigned to a parent-class reference.

Example:

Dog dog = new Dog();

Animal animal = dog;

It can also be written directly:

Animal animal = new Dog();

Upcasting is safe because every Dog is an Animal.

The parent reference can access members available through the parent type:

animal.sound();

If the method is overridden, the child's implementation is executed at runtime.


Downcasting

Downcasting occurs when a parent reference is explicitly converted back to a child reference.

Example:

Animal animal = new Dog();

Dog dog = (Dog) animal;

Now the Dog-specific members can be accessed:

dog.bark();

However, downcasting must be used carefully.

This is unsafe:

Animal animal = new Cat();

Dog dog = (Dog) animal;

It causes a ClassCastException at runtime because the actual object is a Cat, not a Dog.

You can check the object's type using instanceof:

if (animal instanceof Dog) {
    Dog dog = (Dog) animal;
    dog.bark();
}

Modern Java also supports pattern matching for instanceof:

if (animal instanceof Dog dog) {
    dog.bark();
}

Compile-Time vs Runtime Polymorphism

Compile-Time Polymorphism Runtime Polymorphism
Usually achieved through method overloading Usually achieved through method overriding
Method selection occurs during compilation Overridden method selection occurs at runtime
Does not require inheritance Commonly uses inheritance or interfaces
Example: add(int, int) and add(double, double) Example: Animal animal = new Dog()
Also called static polymorphism Also called dynamic polymorphism

Polymorphism and Method Overloading

Method overloading allows the same method name to handle different parameter types or numbers of parameters.

Example:

class Printer {

    void print(int value) {
        System.out.println("Integer: " + value);
    }

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

Usage:

Printer printer = new Printer();

printer.print(100);
printer.print("Hello");

The compiler chooses the appropriate method based on the arguments.


Polymorphism and Method Overriding

Method overriding allows subclasses to change inherited behavior.

Example:

class Employee {

    void work() {
        System.out.println("Employee is working");
    }
}

class Developer extends Employee {

    @Override
    void work() {
        System.out.println("Developer is writing code");
    }
}

Now:

Employee employee = new Developer();

employee.work();

Output:

Developer is writing code

This is runtime polymorphism.


Static Methods and Polymorphism

Static methods are associated with the class rather than a particular object.

They are hidden, not overridden.

Example:

class Parent {

    static void display() {
        System.out.println("Parent");
    }
}

class Child extends Parent {

    static void display() {
        System.out.println("Child");
    }
}

Now:

Parent obj = new Child();

obj.display();

Output:

Parent

The method is selected based on the reference type because static methods are resolved at compile time.

This differs from instance method overriding.


Private Methods and Polymorphism

Private methods are not inherited by subclasses, so they cannot be overridden.

Example:

class Parent {

    private void display() {
        System.out.println("Parent");
    }
}

class Child extends Parent {

    void display() {
        System.out.println("Child");
    }
}

The display() methods are not an overriding relationship because the parent method is private.

They are separate methods.


Final Methods and Polymorphism

A final method cannot be overridden.

Example:

class Parent {

    final void display() {
        System.out.println("Parent");
    }
}

This is invalid:

class Child extends Parent {

    @Override
    void display() {
        System.out.println("Child");
    }
}

The compiler reports an error because a final method cannot be overridden.


Covariant Return Types

Java allows an overriding method to return a subtype of the return type declared by the parent method.

Example:

class Animal {
}

class Dog extends Animal {
}

class Parent {

    Animal getAnimal() {
        return new Animal();
    }
}

class Child extends Parent {

    @Override
    Dog getAnimal() {
        return new Dog();
    }
}

The Dog return type is a subtype of Animal, so this is a valid covariant return type.


Real-World Example: Notification System

Polymorphism is useful when different implementations follow the same contract.

interface Notification {

    void send(String message);
}

Email implementation:

class EmailNotification implements Notification {

    @Override
    public void send(String message) {
        System.out.println("Sending email: " + message);
    }
}

SMS implementation:

class SmsNotification implements Notification {

    @Override
    public void send(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

The application can work with the interface:

public class Main {

    public static void main(String[] args) {

        Notification notification;

        notification = new EmailNotification();
        notification.send("Welcome!");

        notification = new SmsNotification();
        notification.send("Your OTP is 1234");
    }
}

Output:

Sending email: Welcome!
Sending SMS: Your OTP is 1234

The calling code uses the same Notification interface while the actual behavior depends on the implementation.


Benefits of Polymorphism

1. Flexible Code

The same code can work with different implementations.

2. Loose Coupling

Code can depend on interfaces or abstract classes rather than concrete implementations.

3. Extensibility

New implementations can often be added without changing code that works with the common abstraction.

4. Code Reuse

Common contracts and behavior can be defined once and reused across implementations.

5. Supports Clean Design

Polymorphism is an important part of object-oriented design and is frequently used with interfaces, dependency injection, and design patterns.


Common Mistakes

Confusing Overloading with Overriding

Overloading means multiple methods with the same name but different parameter lists.

void add(int a, int b) {
}

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

Overriding means a subclass provides a new implementation of an inherited method.

@Override
void sound() {
}

Assuming Static Methods Are Overridden

Static methods are hidden, not overridden.

Parent obj = new Child();

obj.display();

For a static method, the parent version is selected based on the reference type.

Unsafe Downcasting

Do not assume that every parent reference refers to a particular child type.

Animal animal = new Cat();

Dog dog = (Dog) animal;

This causes ClassCastException.

Forgetting @Override

Using:

@Override

helps the compiler verify that a method correctly overrides a parent method.


Key Points to Remember

  • Polymorphism means one interface or operation can have multiple forms of behavior.
  • Java supports compile-time and runtime polymorphism.
  • Method overloading is the common example of compile-time polymorphism.
  • Method overriding is the common example of runtime polymorphism.
  • Runtime polymorphism relies on dynamic method dispatch.
  • A parent reference can refer to a child object.
  • Upcasting is generally safe.
  • Downcasting should be performed carefully.
  • Interfaces and abstract classes are commonly used to achieve runtime polymorphism.
  • Static methods are hidden rather than overridden.
  • Private methods cannot be overridden.
  • Final methods cannot be overridden.
  • Polymorphism promotes flexible and loosely coupled designs.

Conclusion

Polymorphism in Java allows the same interface or method call to produce different behavior depending on the context or actual object. Compile-time polymorphism is commonly achieved through method overloading, while runtime polymorphism is achieved through method overriding.

Polymorphism becomes especially powerful when combined with inheritance, interfaces, abstract classes, and dependency injection, allowing applications to work with abstractions instead of being tightly coupled to specific implementations.

Related

Found a problem on this page, or something that could be clearer?

Goes straight to the author. Not published anywhere.