Java Polymorphism: Overloading, Overriding, and Dynamic Dispatch

Learn Java polymorphism with runnable examples of overriding and overloading, declared versus runtime types, null overloads, static hiding, and interface contracts.

Last reviewed: November 2026

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Runtime polymorphism lets an overridden instance method execute according to the actual object type, even through a parent or interface reference. Overloading is different: the compiler selects a method signature using declared argument types before runtime dispatch selects an implementation of that signature.

🔍 What Is It?

Java has two types of polymorphism:

1. Runtime Polymorphism (Dynamic Dispatch)

Method overriding — the most specific implementation is called based on the actual runtime type. This is what most people mean by polymorphism.

2. Compile-time Polymorphism (Method Overloading)

Multiple methods with the same name but different parameter lists. The compiler selects the correct one based on the argument types.

Key Mechanism:

The language defines observable dispatch behavior, not one mandatory lookup structure. A JVM may use method tables, inline caches, or optimized direct calls while preserving the specified method selection rules.

Overloading versus overriding

QuestionOverloadingOverriding
What changes?Parameter types or arityImplementation of an inherited instance method
When is the signature chosen?Compile timeThe signature was already chosen at compile time
What decides the implementation?The selected declaration; instance dispatch may still applyRuntime receiver type
Can only the return type distinguish methods?NoCovariant reference returns are permitted

Covariant Return Types:

A subclass can override a method and return a more specific type than the parent declared.

Dispatch limits

  • →Static methods are hidden, not overridden; use the declaring class name to call them clearly.
  • →Fields are selected by the reference's compile-time type, not dynamically dispatched.
  • →Private methods are not inherited as override targets; final methods cannot be overridden.
  • →Constructors are not overridden.
  • →Runtime overriding can be observed through either a parent/interface reference or a concrete reference; a parent-typed variable is a useful demonstration, not a requirement.

🎯 Why Do We Need It?

Polymorphism is the foundation of extensible software:

  • →Open/Closed Principle — code is open for extension (add new types) but closed for modification (don't change existing code)
  • →Dependency Inversion — depend on abstractions (interfaces), not concrete classes
  • →Strategy Pattern — swap algorithms at runtime by swapping implementations
  • →Framework design — a Spring client can depend on an interface while a configured implementation supplies the behavior

Without polymorphism, adding a new payment type requires modifying every place that processes payments. With polymorphism, you add a new class and the existing code works automatically.

🏗️ What Happens in Real Projects?

Every Spring Boot application uses polymorphism constantly:

  • →@Service classes implement service interfaces — controllers depend on the interface
  • →@Repository extends JpaRepository — Spring generates the implementation at runtime
  • →Exception handlers use polymorphism — catch(Exception e) catches all subtypes
  • →Notification systems — EmailNotifier, SMSNotifier, PushNotifier all implement Notifier

⚙️ How It Works

Method invocation has two important stages:

  1. At compile time, Java checks the reference type, accessible declarations, argument types, and overload rules to select a method signature.
  2. At runtime, an ordinary overridden instance method is dispatched using the receiver object's actual class. Static calls do not use this dispatch rule.

If Person person = new Student(), then person.speak() can execute Student's override. But an overloaded describe(person) is selected using the declared Person argument type; Java does not switch to a Student overload merely because the argument holds a Student object.

For overloads accepting int, Integer, and long, process(null) selects the reference-type Integer overload. Null cannot be passed directly to a primitive parameter. With unrelated reference overloads such as String and Integer, select(null) is ambiguous unless an explicit type disambiguates it.

💻 Example

Polymorphism in a Notification System — How polymorphism makes adding new notification channels trivial.

java
public class PolymorphismDemo {
    static class Person {
        public String speak() { return "Person speaking"; }
    }

    static class Student extends Person {
        @Override
        public String speak() { return "Student speaking"; }
    }

    static String describe(Person value) { return "Person overload"; }
    static String describe(Student value) { return "Student overload"; }

    static String process(int value) { return "int overload"; }
    static String process(Integer value) { return "Integer overload"; }
    static String process(long value) { return "long overload"; }

    public static void main(String[] args) {
        Person person = new Student();
        System.out.println(person.speak());           // Student speaking
        System.out.println(describe(person));         // Person overload
        System.out.println(describe((Student) person)); // Student overload
        System.out.println(process(null));           // Integer overload
    }
}

🤔 Common Confusions

❓ What is the difference between overriding and overloading?

Overloading defines same-named methods with different parameter lists and can include inherited methods. Overload selection occurs at compile time. Overriding supplies a compatible implementation of an inherited instance method; runtime receiver type chooses the implementation of the selected signature. Use @Override to have the compiler check an intended override.

❓ Can static methods be polymorphic?

No. Static methods are resolved at compile time based on the declared reference type, not the actual runtime type. This is called method hiding, not overriding. If you call Animal.speak() vs Dog.speak(), you always get the declared type's version. This is why polymorphism requires instance methods.

❓ What is the difference between instanceof and polymorphism?

An interface method can replace repeated type checks when each implementation owns the behavior. instanceof or pattern matching can still be appropriate at boundaries or when working with a closed set of variants. Choose according to who owns the behavior rather than treating every type test as an error.

🚨 Common Production Issues

instanceof chains instead of polymorphism — long if/else instanceof chains violate OCP and become unmaintainable. Refactor to polymorphism.

Overriding equals in a hierarchy — if Parent.equals() is based on type, subclass instances can break symmetry (a.equals(b) is true but b.equals(a) is false). Effective Java recommends against overriding equals in subclasses that add fields.

Covariant return type confusion — a method returning a subtype can confuse callers who expect the parent type. Document clearly.

✅ Best Practices

Program to interfaces, not concrete classes — List<String> not ArrayList<String>.

Use polymorphism to eliminate instanceof chains.

Keep the Liskov Substitution Principle — subclasses must be substitutable for parent.

Prefer interface-based polymorphism over class-based inheritance polymorphism.

Use @Override always — it catches signature mismatches at compile time.

🏛️ Architect's Note

Senior Architect's Perspective

An interface can separate a caller from interchangeable implementations, which is useful for strategies, external integrations, and testing. Spring can inject either interfaces or concrete classes; an interface is a design choice, not a dependency-injection requirement. Keep each implementation substitutable for the contract, including failures and side effects, rather than only matching method signatures.

💬 Frequently Asked Questions

What is the difference between an abstract class and an interface for polymorphism?

Both enable polymorphism, but: abstract classes can have state (fields) and constructors; interfaces cannot (except constants). A class can implement multiple interfaces but extend only one abstract class. Use abstract class when subclasses share common state or implementation. Use interface when you want to define a contract that unrelated classes can implement. In modern Java (Java 8+), interfaces with default methods blur this distinction.

What is dynamic dispatch and how does it work?

Dynamic dispatch selects the implementation of an overridden instance method according to the actual receiver object. JVMs can optimize calls while preserving that behavior. The Java language does not require one particular table structure or guarantee a fixed dispatch cost.

Does overloading use an argument object’s runtime type?

No. The compiler selects an overload using declared argument types and overload applicability rules. With Person person = new Student(), describe(person) selects a Person overload if that is the applicable declaration. An override of the selected instance signature can still be dispatched at runtime.

Is process(null) ambiguous for int, Integer, and long overloads?

No. Null is applicable to the Integer reference overload, not directly to primitive int or long parameters, so Integer is selected. If unrelated reference overloads such as String and Integer are available, a null argument can be ambiguous.

Are fields and static methods dynamically dispatched?

No. Fields are selected using the compile-time reference type, and static methods are hidden rather than overridden. An ordinary overridden instance method is the case where runtime receiver type selects the implementation.

Can an overriding method change its return type?

It can narrow a reference return type covariantly, for example from Person to Student. It cannot choose an unrelated return type or distinguish overloaded methods only by their return types. Override access and checked-exception rules also still apply.

Do I need a framework to demonstrate polymorphism?

No. A parent class or interface and two implementations are enough. A small main method that calls an overridden method through a parent reference demonstrates runtime dispatch without Spring or any external dependency.

🎯 Key Takeaways

1Runtime polymorphism: method called based on actual object type, not reference type
2Compile-time polymorphism: method overloading resolved by compiler based on argument types
3Always use @Override to catch overriding mistakes at compile time
4Program to interfaces — polymorphism works through interface references
5Polymorphism eliminates instanceof chains — use it to follow the Open/Closed Principle
6Static methods are NOT polymorphic — they are resolved at compile time

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages