Polymorphism in Java

  • Last Updated: September 3, 2025
  • By: javahandson
  • Series
img

Polymorphism in Java

Understand polymorphism in Java with examples. Learn about method overloading, overriding, dynamic dispatch, and how polymorphism helps write flexible and reusable code.

Polymorphism in Java is one of the core Object-Oriented Programming (OOP) principles, and it means ‘one name multiple forms’. It allows objects to behave differently based on their actual runtime type, even if they are accessed through a common interface or superclass. Polymorphism allows a single action to behave differently on different classes.

Ex. Think of a TV remote – the same remote can control a TV, a music system, or a projector. Even though we press the same “Power” button, the underlying action depends on the device.

Importance of Polymorphism

Polymorphism is a core pillar of Object-Oriented Programming (OOP) and plays a vital role in building flexible, maintainable, and scalable applications in Java.

1. Code Reusability – We can write general code that works with a parent class or interface, and reuse it for any subclass. For example, a method that accepts a Teacher can work with MathsTeacher or ScienceTeacher objects.

2. Cleaner and Maintainable Code – We don’t have to write multiple if-else or switch statements to check types. We just call the method and let Java decide which implementation to run at runtime.

3. Scalability and Extensibility – We can add new classes or features without changing existing code. For example, we can add an EnglishTeacher class without touching the existing #teach() method.

4. Supports Design Principles – Follows Open/Closed Principle, i.e., open for extension and closed for modification. Encourages the use of Interfaces and Abstract Classes, promoting loose coupling.

5. Enables Dynamic Method Dispatch – At runtime, Java decides which overridden method to execute based on the actual object. This is runtime polymorphism, and it allows Java to behave smartly during execution.

6. Promotes Interface-based Programming – We can program to an interface, not to a concrete implementation. This leads to more testable and flexible code in enterprise and real-world applications.

Types of Polymorphism

Polymorphism in Java can be broadly classified into two types: Compile-time and Runtime Polymorphism.

Compile-time Polymorphism

Compile-time Polymorphism is also known as static Polymorphism. It is a type of polymorphism where the method to be executed is determined at compile time. It is achieved using method overloading, where multiple methods have the same name but different parameter lists.

package com.javahandson;
class Addition {
    int add(int a, int b) {
        return a + b;
    }
    double add(double a, double b) {
        return a + b;
    }
    int add(int a, int b, int c) {
        return a + b + c;
    }
}
public class Sample {
    public static void main(String[] args) {
        Addition addition = new Addition();
        System.out.println(addition.add(5, 10));
        System.out.println(addition.add(5.00, 10.00));
        System.out.println(addition.add(5, 10, 15));
    }
}
Output:
15
15.0
30

The above code defines three methods named #add. They perform addition, but each accepts different parameters – this is known as method overloading, and it’s a type of compile-time polymorphism.

All three methods have the same name: #add, but they behave differently depending on the number and type of arguments.

Runtime Polymorphism

Runtime Polymorphism is also known as dynamic Polymorphism. It is a type of polymorphism where the method to be executed is determined at runtime, based on the actual object’s type. It is achieved using method overriding, where a subclass provides a specific implementation of a method defined in the superclass.

For example, imagine a teacher who teaches Mathematics and Science. As students, we simply ask our teachers to ‘teach’, but what the teacher teaches depends on their specialization.

package com.javahandson;
class Teacher {
    void teach() {
        System.out.println("Teacher is teaching a subject");
    }
}
class MathsTeacher extends Teacher {
    void teach() {
        System.out.println("MathsTeacher is teaching algebra");
    }
}
class ScienceTeacher extends Teacher {
    void teach() {
        System.out.println("Science teacher is teaching Physics");
    }
}

public class Main {
    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        teacher.teach();

        Teacher mathsTeacher = new MathsTeacher();
        mathsTeacher.teach();

        Teacher scienceTeacher = new ScienceTeacher();
        scienceTeacher.teach();
    }
}
Output:
Teacher is teaching a subject
MathsTeacher is teaching algebra
Science teacher is teaching Physics

In the above example, A teacher is the common reference type. Based on the teacher object we assign MathsTeacher or ScienceTeacher, the #teach() method behaves differently. We don’t have to write separate code for each teacher – just call #teach() on a Teacher reference – this is polymorphism.

Interface and abstract class usage

Polymorphism is most powerful when used with interfaces and abstract classes, because they allow us to write generic code that works with different implementations.

Using interfaces for polymorphism

An interface defines a contract, i.e., a set of methods, and multiple classes can implement it in their own way. We can use the interface type as a reference and call the method – the actual method executed depends on the object.

package com.javahandson;

interface Teacher {
    void teach();
}

class MathsTeacher implements Teacher {
    public void teach() {
        System.out.println("MathsTeacher is teaching algebra");
    }
}

class ScienceTeacher implements Teacher {
    public void teach() {
        System.out.println("Science teacher is teaching Physics");
    }
}

public class Main {
    public static void main(String[] args) {

        // Teacher teacher = new Teacher();  java: com.javahandson.Teacher is abstract; cannot be instantiated

        Teacher mathsTeacher = new MathsTeacher();
        mathsTeacher.teach();

        Teacher scienceTeacher = new ScienceTeacher();
        scienceTeacher.teach();
    }
}
Output:
MathsTeacher is teaching algebra
Science teacher is teaching Physics

Here, Teacher is the interface, and even though we are using it as a reference type, the actual method is decided at runtime – it is a classic example of runtime polymorphism.

Using Abstract Classes for Polymorphism

An abstract class can contain both abstract and concrete methods. Subclasses provide implementation for abstract methods.

package com.javahandson;
abstract class Teacher {
    void teach() {
        System.out.println("Teacher is teaching a subject");
    }
    abstract void subject();
}
class MathsTeacher extends Teacher {
    void subject() {
        System.out.println("MathsTeacher is teaching algebra");
    }
}
class ScienceTeacher extends Teacher {
    void subject() {
        System.out.println("Science teacher is teaching Physics");
    }
}

public class Main {
    public static void main(String[] args) {
        // Teacher teacher = new Teacher(); java: com.javahandson.Teacher is abstract; cannot be instantiated

        Teacher mathsTeacher = new MathsTeacher();
        mathsTeacher.teach();
        mathsTeacher.subject();

        Teacher scienceTeacher = new ScienceTeacher();
        scienceTeacher.teach();
        scienceTeacher.subject();
    }
}
Output:
Teacher is teaching a subject
MathsTeacher is teaching algebra
Teacher is teaching a subject
Science teacher is teaching Physics

This example shows runtime polymorphism using an abstract class. The Teacher class defines an abstract method #subject() which is implemented differently by MathsTeacher and ScienceTeacher. At runtime, the actual object, i.e., MathsTeacher or ScienceTeacher, decides which version of #subject() to call, while the #teach() method is inherited and used directly. This allows flexible and reusable code.

In summary, interfaces and abstract classes allow us to write polymorphic code. We can treat different objects the same way, and Java figures out the actual method at runtime. This leads to extensible, maintainable, and flexible software.

Upcasting and dynamic method dispatch

Upcasting means converting a subclass object reference into a superclass reference and this happens implicitly in Java.

Teacher mathsTeacher = new MathsTeacher(); // Upcasting

Here, MathsTeacher is upcast to Teacher.

Dynamic method dispatch is a mechanism by which a call to an overridden method is resolved at runtime, not compile time. It works when a superclass reference points to a subclass object (via upcasting). The actual method that gets executed depends on the object type, not the reference type.

Dynamic method dispatch ensures the correct overridden method is called based on the object’s actual type at runtime.

Teacher mathsTeacher = new MathsTeacher();

mathsTeacher.subject();    // Here the subject method of MathsTeacher is called  

Rules for method overloading

1. Methods must have different parameter lists; they can differ in:

– number of parameters Ex. #add(int, int) Vs #add(int, int, int)
– types of parameters Ex. #add(int, int) Vs #add(double, double)
– order of parameters Ex. #add(int, double) Vs #add(double, int)

2. If the parameters are the same and only the return type is different, then we will get a compile-time error.

package com.javahandson;

class Addition {
    int add(int a, int b) {
        return a + b;
    }
    double add(int a, int b) {
        return a + b;
    }
}
public class Sample {
    public static void main(String[] args) {
        Addition addition = new Addition();
    }
}
Output: Compile time error: java: method add(int,int) is already defined in class com.javahandson.Addition

3. We can specify different access modifiers like public, private, and protected when defining an overloaded method.

package com.javahandson;

class Addition {
    public int add(int a, int b) {
        return a + b;
    }
    // As this is private method hence it cannot be directly accessed in different class
    private double add(double a, double b) {
        return a + b;
    }
    double getAddition(double a, double b) {
        return add(a, b);
    }
}
public class Sample {
    public static void main(String[] args) {
        Addition addition = new Addition();
        System.out.println(addition.add(5, 10));
        System.out.println(addition.getAddition(5.00, 10.00)); // This indirectly calls the add method with double return type
    }
}
Output:
15
15.0

4. We can overload static, final, and even private methods. Overloading works with any method type.

package com.javahandson;

class Addition {
    static int add(int a, int b) {
        return a + b;
    }
    final double add(double a, double b) {
        return a + b;
    }
}
public class Sample {
    public static void main(String[] args) {
        Addition addition = new Addition();
        System.out.println(addition.add(5, 10));
        System.out.println(addition.add(5.00, 10.00));
    }
}
Output:
15
15.0

5. Overloading can happen in the same class or in a subclass as long as parameter lists differ.

package com.javahandson;

class Addition {
    int add(int a, int b) {
        return a + b;
    }
}
public class Sample extends Addition {
    double add(double a, double b) {
        return a + b;
    }

    public static void main(String[] args) {
	Addition addition = new Sample();
        System.out.println(addition.add(5, 10));
       // System.out.println(addition.add(5.00, 10.00));   // not possible ( Dynamic method dispatch )
        
        Sample sample = new Sample();
        System.out.println(sample.add(5.00, 10.00));
    }
}
Output:
15
15.0

Dynamic method dispatch is not possible in method overloading because the method to call is decided at compile time based on:

  • The reference type
  • The parameter list (number, type, order)
  • The runtime object type has no effect in overloading.

When we call addition.add(5.00, 10.00), even though the ‘addition’ reference actually points to a ‘Sample’ object at runtime, the compiler decides which method to invoke based on the reference type ‘Addition’ and the parameter types (double, double). Since no matching method exists in the ‘Addition’ class with these parameters, the compiler throws an error at compile time – the runtime object type ‘Sample’ is irrelevant in method overloading.

Rules for method overriding

Method overriding is runtime polymorphism, where a subclass provides its own implementation of a method defined in its superclass. Here are the rules we must follow:

1. Same Method Signature – The method name and parameter list must be exactly the same in the subclass as in the superclass.

class Teacher { void teach() {} }

class MathsTeacher extends Teacher { void teach() {} }

2. Return Type rules – Must have the same return type as the overridden method, or a covariant return type (a subclass of the original return type).

package com.javahandson;

class Parent {
    Number add(int a, int b) {
        System.out.println("Parent class method called");
        return a + b;
    }
}
class Child extends Parent {
    Integer add(int a, int b) {
        System.out.println("Child class method called");
        return a + b;
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output:
Child class method called
15

In the above example Parent class #add method returns Number, whereas in the Child class, the overridden #add method returns Integer, which is a subclass of Number, or a covariant return type, hence it works.

3. Access Modifier Rule – We cannot reduce visibility of the overridden method. We can maintain the visibility or increase visibility.

Ex. When visibility is increased from protected to public ( works fine )

package com.javahandson;

class Parent {
    protected int add(int a, int b) {
        return a + b;
    }
}
class Child extends Parent {
    public int add(int a, int b) {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: 30

Ex. When visibility is decreased from protected to default ( exception )

package com.javahandson;

class Parent {
    protected int add(int a, int b) {
        return a + b;
    }
}
class Child extends Parent {
    int add(int a, int b) {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: java: add(int,int) in com.javahandson.Child cannot override add(int,int) in com.javahandson.Parent
  attempting to assign weaker access privileges; was protected

4. Exception Rule – We can throw unchecked exceptions freely while overriding methods; there is no restriction on throwing unchecked exceptions. But with checked exceptions, there are a few restrictions, like:

We have to throw the same exception, or throw a subclass of the exception declared in the parent method, or skip throwing an exception.

Ex. Throwing unchecked exceptions ( no restriction )

package com.javahandson;

class Parent {
    int add(int a, int b) throws ArithmeticException {
        return a + b;
    }
}
class Child extends Parent {
    int add(int a, int b) throws NullPointerException {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: 30

Ex. Throwing no checked exceptions in subclass ( works )

package com.javahandson;

import java.io.IOException;

class Parent {
    int add(int a, int b) throws IOException {
        return a + b;
    }
}
class Child extends Parent {
    int add(int a, int b) {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) throws IOException {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: 30

Ex. Throwing the same checked exceptions in a subclass ( works )

package com.javahandson;

import java.io.IOException;

class Parent {
    int add(int a, int b) throws IOException {
        return a + b;
    }
}
class Child extends Parent {
    int add(int a, int b) throws IOException {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) throws IOException {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: 30

Ex. Throws a Subclass of the checked exception ( works )

package com.javahandson;

import java.io.FileNotFoundException;
import java.io.IOException;

class Parent {
    int add(int a, int b) throws IOException {
        return a + b;
    }
}
class Child extends Parent {
    int add(int a, int b) throws FileNotFoundException {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) throws IOException {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: 30

Ex. Throws a superclass of the checked exception ( does not work )

package com.javahandson;

import java.io.IOException;

class Parent {
    int add(int a, int b) throws IOException {
        return a + b;
    }
}
class Child extends Parent {
    int add(int a, int b) throws Exception {
        return 2 * (a + b);
    }
}
public class Sample {
    public static void main(String[] args) throws IOException {
        Parent parent = new Child();
        System.out.println(parent.add(5, 10));
    }
}
Output: java: add(int,int) in com.javahandson.Child cannot override add(int,int) in com.javahandson.Parent
  overridden method does not throw java.lang.Exception

5. Static Methods Are Not Overridden

Static methods belong to the class, not the object, so they cannot be overridden. If a subclass defines a static method with the same signature, it does not override but instead hides the superclass method. The version of the static method that gets executed depends only on the reference type at compile time, not on the actual object type at runtime.

package com.javahandson;

class Parent {
    static void display() {
        System.out.println("Parent static method");
    }
}
class Child extends Parent {
    static void display() {
        System.out.println("Child static method");
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent obj1 = new Parent();
        Parent obj2 = new Child(); // runtime object is Child
        Child obj3 = new Child();

        obj1.display(); // Parent static method
        obj2.display(); // Parent static method (reference type = Parent)
        obj3.display(); // Child static method
    }
}
Output:
Parent static method
Parent static method
Child static method

6. Final Methods Cannot Be Overridden

A method declared with the final keyword cannot be overridden by subclasses. This ensures the implementation in the superclass remains unchanged and is inherited “as-is.”

package com.javahandson;

class Parent {
    final void display() {
        System.out.println("Parent method");
    }
}
class Child extends Parent {
    void display() {
        System.out.println("Child method");
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent obj1 = new Parent();
    }
}
Output:
java: display() in com.javahandson.Child cannot override display() in com.javahandson.Parent
  overridden method is final

7. Constructors Cannot Be Overridden

Constructors are not inherited in Java, and hence they cannot be overridden. Each class defines its own constructors, and the subclass does not reuse or modify the parent’s constructors.

Constructors are tied to the class that defines them; overriding would break object initialization rules. The purpose of constructors is to initialize new objects, whereas methods define behavior.

package com.javahandson;

class Parent {
    Parent() {
        System.out.println("Parent constructor");
    }
}
class Child extends Parent {
    Child() {
        super(); // calls Parent constructor (must be first line)
        System.out.println("Child constructor");
    }
}
public class Sample {
    public static void main(String[] args) {
        Parent parent = new Child();
    }
}
Output:
Parent constructor
Child constructor

8. Overriding and Private Methods

Private methods cannot be overridden because they are not visible to subclasses (they are accessible only within the class where they are declared). If a subclass defines a method with the same name and signature, it is treated as a new method (not an override).

package com.javahandson;

class Parent {
    private void display() {
        System.out.println("Parent private method");
    }
    void callDisplay() {
        display(); // calls Parent's private method
    }
}
class Child extends Parent {
    private void display() {
        System.out.println("Child private method");
    }
    void callChildDisplay() {
        display(); // calls Child's private method
    }
}
public class Sample {
    public static void main(String[] args) {
        Child child = new Child();
        child.callDisplay();        // Parent private method

        child.callChildDisplay(); // Child private method
    }
}
Output:
Parent private method
Child private method

In the above example, Parent.display() and Child.display() are two independent methods. The child’s #display() doesn’t override the parent’s #display(); it only hides it inside the Child class.

9. Overriding and @Override Annotation

@Override is a marker annotation in Java. It tells the compiler that the method is intended to override a method in the superclass (or an interface default method). If it doesn’t actually override (e.g., due to a typo, wrong parameters, wrong access level), the compiler will throw an error.

Ex. Correct Override

package com.javahandson;

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

Example – Incorrect @Override

package com.javahandson;

class Parent {
    void show() {
        System.out.println("Parent show()");
    }
}
class Child extends Parent {
    @Override
    void show(int number) {
        System.out.println("Child show()");
    }
}
Output: Compile-time error java: method does not override or implement a method from a supertype

Without @Override, the above code compiles fine, but we intended to override and mistakenly overloaded. With @Override, the compiler flags an error, helping us catch bugs early.

Advantages of polymorphism

1. Code Reusability – We can write general methods that can work with different types of objects through a common interface or superclass.

2. Extensibility – New classes can be added without modifying existing code, making applications easy to extend and scale.

3. Cleaner and Maintainable Code – Eliminates the need for complex if-else or switch statements for handling different object types.

4. Supports OOP Principles – Encourages abstraction, loose coupling, and interface-driven design (e.g., programming to an interface).

5. Runtime Flexibility – Through dynamic method dispatch, Java decides at runtime which method implementation to invoke, making the program more flexible.

Best practices

1. Program to an Interface, not an Implementation – Always use parent classes or interfaces as reference types, so code works with any subclass implementation.

2. Use @Override Annotation – Ensure methods are correctly overridden; this prevents signature mismatch errors.

3. Avoid Unnecessary Downcasting – Prefer polymorphic method calls instead of casting objects back to their original type.

4. Leverage Abstraction – Use abstract classes or interfaces to define common contracts and achieve flexibility.

5. Keep Methods Focused – Override only when it makes logical sense; don’t override just to change a small behavior that can be reused from the parent class.

Conclusion

Polymorphism in Java is one of the most powerful features of Java and a cornerstone of object-oriented programming. It allows a single interface to represent multiple forms, enabling code reusability, flexibility, and cleaner design. With method overloading (compile-time polymorphism) and method overriding (runtime polymorphism), developers can write programs that are both extensible and easy to maintain. By following best practices such as programming to interfaces and avoiding unnecessary downcasting, polymorphism can help build scalable, modular, and future-proof applications.

FAQs

What is polymorphism in Java?

Polymorphism in Java is the ability of an object to take many forms. It allows the same method call to behave differently depending on the object’s type.

What are the types of polymorphism in Java?

There are two main types:
Compile-time polymorphism (method overloading)
Runtime polymorphism (method overriding)

What is the difference between overloading and overriding?

Overloading happens at compile time with the same method name but different parameter lists.
Overriding happens at runtime when a subclass provides a new implementation of a method in the parent class.

Can static methods be overridden in Java?

No, static methods cannot be overridden. They can only be hidden by declaring another static method with the same signature in the subclass.

What are the advantages of polymorphism?

Polymorphism promotes code reusability, scalability, cleaner design, and runtime flexibility, making programs easier to maintain and extend.

Leave a Comment