Polymorphism in Java

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

Polymorphism in Java

Polymorphism in Java lets one method name take many forms. This guide covers method overloading and method overriding. It explains early and late binding, upcasting, and dynamic method dispatch. It also covers interfaces, abstract classes, the traps that catch beginners, and ten interview questions.

1. Introduction

Polymorphism in Java is the pillar of object-oriented programming that most people meet last and understand slowest. The word itself sounds heavy. The idea behind it is refreshingly simple.

Poly means many. Morph means form. So polymorphism just means “many forms”. In Java it lets one method name do several different jobs, and it lets one reference variable drive several different objects.

Why should you care? Because polymorphism is what stops your code turning into a swamp of if-else checks. Write your logic once against a general type, and every new subclass slots in for free.

1.1 What This Article Covers

  • What polymorphism means, in plain language
  • Method overloading, and how the compiler picks a method
  • Method overriding, and how the JVM picks a method
  • Early binding and late binding
  • Upcasting, and how a call finds the right body
  • Every rule the compiler enforces on both
  • What you can never override, and why
  • Interfaces, abstract classes, and when to pick each
  • Ten interview questions with short, direct answers

2. What Is Polymorphism?

Polymorphism lets a single action behave differently depending on what it acts upon. The caller writes one line. The object decides what that line actually does.

2.1 A Simple Analogy

Think about the power button on a universal remote. You press the same button every time. Point it at a TV and the screen wakes up. Point it at a music system and the speakers come alive.

One button, many outcomes. The remote never asks which device it faces. The device answers in its own way.

Java works the same way. You call teach() on a Teacher reference, and the object behind that reference decides whether algebra or physics comes out.

2.2 One Name, Many Forms

Java gives us two very different ways to reach that goal.

  • Method overloading puts several methods with the same name in one class, each taking different parameters. The compiler chooses among them.
  • Method overriding puts a fresh version of a parent method inside a subclass. The JVM chooses between them while the program runs.

Both wear the label “polymorphism”, yet they solve different problems. Mixing them up is the single most common interview stumble on this topic.

2.3 Why Polymorphism Matters

  • Reuse. One method that accepts a Teacher handles every kind of teacher you ever add.
  • Cleaner code. Long if-else chains that test types simply disappear.
  • Easy growth. Adding an EnglishTeacher class touches no existing file.
  • Open/Closed Principle. Your code stays open to extension and closed to modification.
  • Loose coupling. Code against an interface and swap the class at will.
  • Testing. Slotting a fake object into a test becomes easy.

3. The Two Types of Polymorphism

Java splits polymorphism by when the decision happens. One kind settles at compile time. The other waits until the program runs.

3.1 Compile-Time Polymorphism

This kind has two other names. Some books call it static binding. Others call it early binding. Method overloading is how we get it.

The compiler reads your call, looks at the argument types, and locks in one specific method. That choice never changes afterwards. By the time your program starts, the decision has already been made.

3.2 Runtime Polymorphism

This kind also has other names. You will see it called dynamic binding, or late binding. Method overriding is how we get it.

Here the compiler only checks that the method exists on the reference type. The real choice waits. When the line finally executes, the JVM inspects the actual object and calls that object’s version.

3.3 Static Binding vs Dynamic Binding

Binding simply means linking a method call to a method body. Java binds some calls early and some late.

  • Static binding handles overloaded methods, plus everything marked static, private, or final
  • Dynamic binding handles overridden instance methods, which covers most of the methods you write
  • Field access always uses static binding, a detail we return to in section 7.4

4. Method Overloading in Detail

Overloading gives one method name several parameter lists inside the same class. It exists to keep names simple. Nobody wants to recall addTwoInts(), addTwoDoubles(), and addThreeInts().

4.1 A First Overloading Example

package com.javahandson;

class Calculator {
    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 OverloadDemo {
    public static void main(String[] args) {
        Calculator calc = new Calculator();

        System.out.println(calc.add(5, 10));         // Output: 15
        System.out.println(calc.add(5.0, 10.0));     // Output: 15.0
        System.out.println(calc.add(5, 10, 15));     // Output: 30
    }
}

Three methods share the name add. The compiler tells them apart by counting and typing the arguments at each call site.

4.2 The Rules of Overloading

The parameter list must differ. Java accepts three kinds of difference:

  • Count. add(int, int) against add(int, int, int)
  • Type. add(int, int) against add(double, double)
  • Order. add(int, double) against add(double, int)

Beyond that, the rules are loose:

  • Access modifiers may differ freely, so one version can stay private
  • static, final, and private methods all overload without complaint
  • Return types may differ, as long as the parameters already differ
  • Thrown exceptions carry no restrictions at all
  • A varargs method such as add(int...) counts as its own overload
  • A subclass may overload a method it inherited from its parent

4.3 What Does Not Count as Overloading

Changing only the return type fails. The compiler rejects it flatly:

class Calculator {
    int add(int a, int b) {
        return a + b;
    }
    // double add(int a, int b) {   // does not compile
    //     return a + b;
    // }
    // error: method add(int,int) is already defined in class Calculator
}

Why so strict? Picture the call calc.add(5, 10); with the result thrown away. Java would have no way to tell which version you meant. Renaming the parameters does not help either, since parameter names never form part of a method signature.

4.4 How Java Picks an Overload

Overload resolution runs in three passes, and it stops at the first pass that finds a match.

  • First it tries widening only, so int may become long or double
  • Next it allows boxing, so int may become Integer or Object
  • Only in the final pass does it look at varargs

That order produces a result that surprises almost everyone:

package com.javahandson;

public class ResolutionDemo {
    static void show(long value)    { System.out.println("long version"); }
    static void show(Integer value) { System.out.println("Integer version"); }
    static void show(int... values) { System.out.println("varargs version"); }

    public static void main(String[] args) {
        show(5);   // Output: long version
    }
}

An int literal matches Integer perfectly after boxing. Java still picks long, because widening wins in pass one and boxing never gets a turn. Delete the long version and the Integer version takes over.

Passing null has its own quirk. Java picks the most specific reference type available, so show(String) beats show(Object). Offer two unrelated types such as String and StringBuilder, and the compiler gives up with an “ambiguous” error.

5. Method Overriding in Detail

Overriding replaces a parent method with a subclass version. This is where polymorphism earns its reputation.

5.1 A First Overriding Example

package com.javahandson;

class Teacher {
    void teach() {
        System.out.println("Teacher teaches a subject");
    }
}

class MathsTeacher extends Teacher {
    @Override
    void teach() {
        System.out.println("MathsTeacher teaches algebra");
    }
}

class ScienceTeacher extends Teacher {
    @Override
    void teach() {
        System.out.println("ScienceTeacher teaches physics");
    }
}

public class OverrideDemo {
    public static void main(String[] args) {
        Teacher[] staff = { new Teacher(), new MathsTeacher(), new ScienceTeacher() };

        for (Teacher t : staff) {
            t.teach();
        }
    }
}
// Output: Teacher teaches a subject
// Output: MathsTeacher teaches algebra
// Output: ScienceTeacher teaches physics

Study that loop. It knows nothing about algebra or physics. Every element has the declared type Teacher, yet each one behaves according to the object it really holds.

5.2 Upcasting Sets the Stage

None of this works without upcasting. Storing a subclass object in a superclass reference is what makes the choice interesting.

Teacher maths = new MathsTeacher();   // upcasting, no cast operator needed
maths.teach();                        // Output: MathsTeacher teaches algebra
// maths.checkHomework();             // compile error if only MathsTeacher declares it

Notice the two halves here. The reference type sets which methods you may call. The object type sets which body then runs. Our guide to type casting in Java digs deeper into that distinction.

5.3 Dynamic Method Dispatch

Dynamic method dispatch is the mechanism that resolves an overridden call at runtime. The name sounds intimidating, so let us walk through what the JVM does.

  • The compiler checks that teach() exists on the reference type Teacher
  • At runtime the JVM looks at the object sitting in memory
  • It finds a MathsTeacher, so it runs the MathsTeacher body
  • Your calling code never learns which class won

Under the hood, each class carries a method table, and the JVM jumps through that table. You get all of this for free by writing extends and an override.

5.4 Where You Already Use This

You have used polymorphism many times already. Nobody gave it a name, that is all.

Think about the last time you typed this line:

List<String> names = new ArrayList<>();
names.add("Asha");
System.out.println(names.get(0));   // Output: Asha

That is an upcast. The List type is the contract. The ArrayList object does the work. Swap in a LinkedList tomorrow and the rest of your code will not notice.

The same trick hides in code you touch every day:

  • Print any object and Java runs your toString(), not the one in Object
  • A Comparator sorts one list many ways, with no change to the list
  • Every JDBC driver hides behind the same Connection type
  • Test doubles slot in wherever your code depends on an interface
  • Wrap a stream in a BufferedReader and the source stops mattering

None of that needs new syntax. It is the same idea you just read about, at a larger scale.

6. The Rules of Overriding

Overriding comes with real limits. Break one and the compiler stops you. That is a good thing.

6.1 Signature and Return Type

The method name and parameter list must match the parent exactly. Change a single parameter type and you have written an overload instead, quietly and by accident.

Return types have one soft rule. A child may return a subtype of what the parent returns. Java calls this a covariant return type.

package com.javahandson;

class Parent {
    Number compute() {
        return 10;
    }
}

class Child extends Parent {
    @Override
    Integer compute() {      // covariant return: Integer extends Number
        return 42;
    }
}

public class CovariantDemo {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.compute());  // Output: 42
    }
}

6.2 The Access Modifier Rule

An override can open up access. It can never shut it down.

Picture the ladder: private, then package-private, then protected, then public. Your override may climb the ladder or stay put. Climbing down breaks every caller who reached the method through the parent type.

class Parent {
    protected int twice(int n) { return 2 * n; }
}

class Child extends Parent {
    @Override
    public int twice(int n) { return 4 * n; }   // widening: protected to public, fine
}

class Broken extends Parent {
    // @Override
    // int twice(int n) { return n; }           // narrowing: protected to default
    // error: attempting to assign weaker access privileges; was protected
}

6.3 The Exception Rules

Unchecked exceptions have no rules here. Throw any RuntimeException you like from an override, whatever the parent says.

Checked exceptions are stricter. Your override may:

  • Keep the same checked exception as the parent
  • Swap in a subclass of it, such as FileNotFoundException in place of IOException
  • Declare fewer exceptions, or none at all
  • Never widen it, so Exception in place of IOException fails
package com.javahandson;

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

class Reader {
    void load() throws IOException { }
}

class FileReaderImpl extends Reader {
    @Override
    void load() throws FileNotFoundException { }   // narrower, allowed
}

class QuietReader extends Reader {
    @Override
    void load() { }                                // none at all, allowed
}

class BadReader extends Reader {
    // @Override
    // void load() throws Exception { }            // broader, rejected
    // error: overridden method does not throw java.lang.Exception
}

The logic is easy once you see it from the caller’s seat. Somebody holding a Reader reference wrote a catch block for IOException. A surprise Exception would sail straight past it.

6.4 The @Override Annotation

Add @Override to every override you write. It costs one line and catches a whole category of bug.

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

class Child extends Parent {
    @Override
    void show(int number) {      // typo: an extra parameter
        System.out.println("Child show()");
    }
    // error: method does not override or implement a method from a supertype
}

Drop the annotation and this compiles happily. You would have written a brand new overload, watched Parent.show() run instead of yours, and spent an afternoon hunting the reason.

7. What You Cannot Override

Four things sit outside dynamic dispatch. Each one trips up beginners in a slightly different way.

7.1 Static Methods Are Hidden, Not Overridden

A static method belongs to the class, never to an object. Declare one with the same signature in a subclass and you hide the parent version rather than override it.

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 HidingDemo {
    public static void main(String[] args) {
        Parent p1 = new Parent();
        Parent p2 = new Child();   // the object really is a Child
        Child  c1 = new Child();

        p1.display();   // Output: Parent static method
        p2.display();   // Output: Parent static method
        c1.display();   // Output: Child static method
    }
}

Look hard at the middle call. The object is a Child, yet the parent version runs, because the reference type decides. That result is the whole difference between hiding and overriding in one line.

7.2 Final and Private Methods

A final method slams the door. No subclass may replace it, and the compiler says so with “overridden method is final”. Authors use final to protect logic that subclasses must not bend.

A private method is invisible outside its own class. A subclass never inherits it, so a same-named method in the child is simply a separate method that happens to share a name.

package com.javahandson;

class Parent {
    private void secret() {
        System.out.println("Parent secret");
    }
    void callSecret() {
        secret();          // always the Parent version
    }
}

class Child extends Parent {
    private void secret() {
        System.out.println("Child secret");
    }
}

public class PrivateDemo {
    public static void main(String[] args) {
        new Child().callSecret();   // Output: Parent secret
    }
}

7.3 Constructors

Constructors never get inherited, so nothing exists to override. A subclass writes its own, and super() chains up to the parent as the first statement.

Two constructors in one class with different parameters do count as overloading, though. That pattern turns up constantly.

7.4 Fields Are Never Polymorphic

Here is the trap that catches even experienced developers. Methods dispatch on the object. Fields resolve on the reference type.

package com.javahandson;

class Parent {
    String label = "Parent field";
    String describe() { return "Parent method"; }
}

class Child extends Parent {
    String label = "Child field";          // hides, does not override
    @Override
    String describe() { return "Child method"; }
}

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

        System.out.println(p.label);       // Output: Parent field
        System.out.println(p.describe());  // Output: Child method
    }
}

Same object, same reference, two opposite answers. Avoid the confusion entirely: keep fields private and expose them through getters, which do dispatch dynamically.

8. Overloading vs Overriding at a Glance

Interviewers love this comparison, so here it is in one table.

Aspect Method Overloading Method Overriding
Polymorphism type Compile-time, static Runtime, dynamic
Binding Early binding Late binding
Where it lives Same class, or a subclass Subclass only
Parameter list Must differ Must match exactly
Return type Free, once parameters differ Same type or a covariant one
Access modifier Any Same or wider
Checked exceptions Unrestricted Same, narrower, or none
Works on static methods Yes No, it hides instead
Inheritance needed No Yes
Decided by Reference and argument types Actual object type

9. Polymorphism With Interfaces and Abstract Classes

Overriding a concrete parent method works. Overriding a contract works better, because the parent then makes no promises about behaviour at all.

9.1 Using Interfaces

An interface declares what a type must do and says nothing about how. Every implementing class fills in the blanks its own way.

package com.javahandson;

interface Teacher {
    void teach();

    default void greet() {          // default method, Java 8 onward
        System.out.println("Good morning, class");
    }
}

class MathsTeacher implements Teacher {
    @Override
    public void teach() {
        System.out.println("MathsTeacher teaches algebra");
    }
}

class ScienceTeacher implements Teacher {
    @Override
    public void teach() {
        System.out.println("ScienceTeacher teaches physics");
    }
}

public class InterfaceDemo {
    public static void main(String[] args) {
        // Teacher t = new Teacher();   // error: Teacher is abstract

        Teacher maths = new MathsTeacher();
        maths.greet();   // Output: Good morning, class
        maths.teach();   // Output: MathsTeacher teaches algebra

        Teacher science = new ScienceTeacher();
        science.teach(); // Output: ScienceTeacher teaches physics
    }
}

Java forbids new Teacher() because an interface has no body to construct. That restriction is the point. It pushes you to depend on the contract instead of a concrete class.

9.2 Using Abstract Classes

An abstract class sits halfway. It can hold shared state and finished methods, plus abstract methods that subclasses must implement.

package com.javahandson;

abstract class Staff {
    String name;

    Staff(String name) {
        this.name = name;
    }

    void checkIn() {                       // shared, already written
        System.out.println(name + " checked in");
    }

    abstract void work();                  // each subclass decides
}

class Librarian extends Staff {
    Librarian(String name) { super(name); }

    @Override
    void work() {
        System.out.println(name + " shelves books");
    }
}

public class AbstractDemo {
    public static void main(String[] args) {
        Staff s = new Librarian("Asha");
        s.checkIn();   // Output: Asha checked in
        s.work();      // Output: Asha shelves books
    }
}

9.3 Choosing Between Them

  • Reach for an interface when unrelated classes share a capability, such as Comparable or Runnable
  • Reach for an abstract class when subclasses share real state and real code
  • Remember that a class implements many interfaces but extends only one class
  • Consider a sealed interface, added in Java 17, when you want to control exactly who implements it

Read our companion guides on the abstract class in Java and on abstraction in Java for the full comparison.

10. Common Mistakes and Pitfalls

Most polymorphism bugs come from a short list. Watch for these:

  • You meant to override, but you wrote a new method. One wrong type in the list is enough. @Override catches it at once.
  • Expecting static methods to dispatch. They never do. The reference type wins every time.
  • Expecting fields to dispatch. Fields hide, they do not override, so p.label follows the declared type.
  • Calling an overridable method from a constructor. The subclass fields have not been assigned yet, so you read null or 0.
  • Downcasting everywhere. Constant instanceof checks mean a method is missing from the parent type.
  • You fix equals() but forget hashCode(). Your objects then break inside every hash set and hash map.
  • Narrowing access in an override. Going from public back to protected refuses to compile.
  • Hoping that overloading gives you runtime choice. It never does. It is just a nicer set of names.

That fourth item deserves a demonstration, because it looks impossible until you see it:

package com.javahandson;

class Base {
    Base() {
        print();                 // calls the overridden version
    }
    void print() {
        System.out.println("Base print");
    }
}

class Derived extends Base {
    String message = "Derived ready";

    @Override
    void print() {
        System.out.println(message);
    }
}

public class ConstructorTrap {
    public static void main(String[] args) {
        new Derived();   // Output: null
    }
}

The parent constructor runs first. It jumps to Derived.print(). But message has no value yet, so null prints. So keep constructors away from methods a child can replace.

11. Putting It All Together

Let us close with one small program that uses every idea above. It sends notifications through different channels, overloads a helper, overrides a contract, and leans on dynamic dispatch.

package com.javahandson;

import java.util.ArrayList;
import java.util.List;

abstract class Notification {
    String recipient;

    Notification(String recipient) {
        this.recipient = recipient;
    }

    abstract void send(String message);          // subclasses override this

    void send(String message, int times) {       // overload, same class
        for (int i = 0; i < times; i++) {
            send(message);                       // dynamic dispatch
        }
    }
}

class EmailNotification extends Notification {
    EmailNotification(String recipient) { super(recipient); }

    @Override
    void send(String message) {
        System.out.println("Email to " + recipient + ": " + message);
    }
}

class SmsNotification extends Notification {
    SmsNotification(String recipient) { super(recipient); }

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

public class NotifyDemo {
    public static void main(String[] args) {
        List<Notification> outbox = new ArrayList<>();
        outbox.add(new EmailNotification("suraj@example.com"));  // upcast
        outbox.add(new SmsNotification("9876543210"));           // upcast

        for (Notification n : outbox) {
            n.send("Your order has shipped");
        }

        outbox.get(1).send("Please rate us", 2);   // overloaded version
    }
}
// Output: Email to suraj@example.com: Your order has shipped
// Output: SMS to 9876543210: Your order has shipped
// Output: SMS to 9876543210: Please rate us
// Output: SMS to 9876543210: Please rate us

Three ideas share one file here. The two-argument send() is compile-time polymorphism, chosen by argument count. The one-argument send() is runtime polymorphism, chosen by the object. And the list upcasts both subclasses to Notification, which is what lets the loop stay so short.

Now add a PushNotification class. You write one new file, add one line to the outbox, and change nothing else. That is the payoff polymorphism promises.

12. Interview Questions

Q: What is polymorphism in Java?

A: Polymorphism in Java lets one method name take many forms. The same call behaves differently depending on the arguments you pass or the object behind the reference.

Q: What are the two types of polymorphism in Java?

A: Compile-time polymorphism comes from method overloading, where the compiler picks the method. Runtime polymorphism comes from method overriding, where the JVM picks it while the program runs.

Q: What is the difference between overloading and overriding?

A: Overloading needs different parameter lists in the same class and resolves at compile time. Overriding needs an identical signature in a subclass and resolves at runtime against the actual object.

Q: Can we override a static method in Java?

A: No. A same-signature static method in a subclass hides the parent version instead. The reference type then decides which one runs, so no dynamic dispatch happens.

Q: What is dynamic method dispatch?

A: Dynamic method dispatch resolves an overridden call at runtime. A superclass reference points at a subclass object, and the JVM runs the subclass version of the method.

Q: Can we overload a method by changing only its return type?

A: No. Two methods with identical names and parameter lists clash, whatever their return types. A call that discards the result would leave the compiler no way to choose.

Q: What is a covariant return type?

A: A covariant return type lets an override return a subclass of the parent’s return type. Returning Integer where the parent returns Number works fine, and Java has allowed this since version 5.

Q: Can an overriding method throw a broader checked exception?

A: No. An override may declare the same checked exception, a subclass of it, or none. Widening to Exception would break callers who only catch the parent’s declared type.

Q: Are fields polymorphic in Java?

A: No. Fields resolve against the reference type at compile time, so a subclass field hides the parent field rather than overriding it. Keep fields private and use getters to get polymorphic behaviour.

Q: Does Java support operator overloading?

A: No. Java deliberately leaves it out to keep code readable. The one exception is + for string concatenation, and the language itself builds that in rather than letting you define it.

13. Conclusion

Let us wrap up what we covered. Polymorphism in Java means one name, many forms, and Java delivers it in two separate ways.

Method overloading is the compile-time half. Several methods share a name inside one class, and the compiler picks one from the argument types. Widening beats boxing, and boxing beats varargs.

Method overriding is the runtime half. A subclass supplies its own body, and the JVM picks it based on the real object. Upcasting sets that up, and dynamic method dispatch carries it out.

Then there are the boundaries. Static methods hide instead of overriding. Fields hide too. Constructors, final methods, and private methods stay off limits entirely.

Interfaces and abstract classes push all of this further. Program against a contract, and every new implementation drops in without touching a line of the code that calls it.

Here is the habit worth building. Whenever you catch yourself writing if (obj instanceof Something), stop and ask whether a method on the parent type would say it better. Nine times out of ten, it would.

Further Reading

Leave a Comment