Java tutorials  /  Java Polymorphism
Chapter 12 · Java

Java Polymorphism

Polymorphism, meaning 'many forms', is an object-oriented concept in Java where a single method name or a single reference type can exhibit different behaviors depending on the context in which it is used. Java implements this through two mechanisms: compile-time polymorphism (method overloading) and runtime polymorphism (method overriding, enabled by dynamic method dispatch).

Think of the word 'drive' as a single command that means something different depending on who or what you're commanding. Telling a car to 'drive' means pressing the accelerator; telling a boat to 'drive' means steering with a wheel and throttle; telling a plane to 'drive' actually means fly. The instruction word stays the same, but the actual action performed depends entirely on the specific type of vehicle receiving it. This is exactly runtime polymorphism: the same method call, 'drive()', produces genuinely different behavior depending on the actual object type executing it.

Consider a payment processing system in an e-commerce application. A generic 'PaymentMethod' reference can point to a 'CreditCard', 'PayPal', or 'UPI' object, and calling 'processPayment()' on this single reference triggers completely different underlying logic depending on which specific payment type the customer actually chose at checkout — credit card processing involves bank authorization, PayPal involves redirecting to their API, and UPI involves generating a QR code. The checkout code itself remains clean and generic, simply calling 'paymentMethod.processPayment()' without needing a separate if-else block checking the payment type manually, which is precisely how real payment gateways like Stripe or Razorpay structure their internal payment-type handling using polymorphism.

Without polymorphism, code that needs to handle multiple related object types (like different shapes, different payment methods, or different employee roles) would require extensive, repetitive if-else or switch chains checking the object's specific type before calling the appropriate type-specific method — a fragile, hard-to-maintain pattern that must be updated everywhere every time a new type is added. Polymorphism solves this by allowing a single, uniform method call to automatically execute the correct type-specific behavior, making code more extensible (new types can be added with zero changes to existing calling code), maintainable, and aligned with clean object-oriented design principles.

  • Compile-Time Polymorphism (Method Overloading): Achieved when multiple methods share the same name but differ in their parameter list (number, type, or order of parameters) within the same class. The compiler determines which specific overloaded method to call based on the arguments provided at the call site, resolved entirely at compile time.
  • Runtime Polymorphism (Method Overriding): Achieved when a subclass provides its own specific implementation of a method that already exists with the exact same signature in its superclass. Which version actually executes is determined at runtime based on the object's actual type, not the reference variable's declared type — a mechanism called dynamic method dispatch.
// Compile-time (Overloading): returnType methodName(int a) { } returnType methodName(int a, int b) { } // Runtime (Overriding): class Parent { void methodName() { } } class Child extends Parent { @Override void methodName() { } }
A beginner wants to build a simple notification system where a generic 'Notification' superclass has a 'send()' method, and specific subclasses 'EmailNotification' and 'SMSNotification' each override this method with their own delivery logic, demonstrating how a single, generic method call can trigger different behaviors depending on the actual notification object type at runtime.
Runtime Polymorphism: Dynamic Method Dispatch
This example demonstrates runtime polymorphism using a Notification superclass and two subclasses, showing how the same method call on a Notification-typed array produces different output based on each object's actual runtime type.
Java
class Notification { public void send(String message) { System.out.println("Sending generic notification: " + message); } } class EmailNotification extends Notification { @Override public void send(String message) { System.out.println("Sending EMAIL: " + message); } } class SMSNotification extends Notification { @Override public void send(String message) { System.out.println("Sending SMS: " + message); } } public class PolymorphismDemo { public static void main(String[] args) { Notification[] notifications = { new EmailNotification(), new SMSNotification() }; for (Notification n : notifications) { n.send("Your order has shipped!"); } } }
Sending EMAIL: Your order has shipped! Sending SMS: Your order has shipped!
The array 'notifications' is declared with the superclass type 'Notification[]', but each element actually holds a specific subclass object (EmailNotification, SMSNotification). During the loop, even though 'n' is declared as type 'Notification', calling 'n.send(...)' at runtime executes each object's ACTUAL overridden version, not the generic superclass version — this dynamic method dispatch is the core mechanism enabling runtime polymorphism, allowing one loop and one method call to correctly handle multiple different notification types.
Compile-Time Polymorphism: Method Overloading for Flexible Input
This example shows compile-time polymorphism through an overloaded 'calculateArea' method that can compute area differently depending on whether one, two, or three arguments are provided.
Java
class AreaCalculator { // For a square double calculateArea(double side) { return side * side; } // For a rectangle double calculateArea(double length, double width) { return length * width; } // For a triangle (base, height, and a flag argument to distinguish overload) double calculateArea(double base, double height, boolean isTriangle) { return 0.5 * base * height; } } public class OverloadingDemo { public static void main(String[] args) { AreaCalculator calc = new AreaCalculator(); System.out.println("Square Area: " + calc.calculateArea(4.0)); System.out.println("Rectangle Area: " + calc.calculateArea(5.0, 3.0)); System.out.println("Triangle Area: " + calc.calculateArea(6.0, 4.0, true)); } }
Square Area: 16.0 Rectangle Area: 15.0 Triangle Area: 12.0
All three methods share the name 'calculateArea', but the Java compiler determines exactly which one to invoke based purely on the number and types of arguments supplied at each call site — this resolution happens entirely at compile time, before the program even runs, which is why compile-time polymorphism is also called static binding, in direct contrast to the dynamic runtime resolution used for method overriding.
  • Attempting to Overload Methods by Return Type Alone: Writing 'int getValue()' and 'double getValue()' within the same class, differing only in return type with identical parameter lists (none, in this case), causes a compile-time error: 'getValue() is already defined'. Java's overload resolution is based entirely on the method's parameter list (number, type, order) — the return type alone is never sufficient to distinguish two overloaded methods.
  • Confusing Overloading (Compile-Time) with Overriding (Runtime) Resolution: Beginners sometimes expect an overridden method to be selected based on the reference variable's declared type, similarly to how overloading works. However, method overriding is resolved at RUNTIME based on the object's actual type, regardless of the reference variable's declared type, which is fundamentally different from overloading's compile-time, argument-based resolution.
  • Overriding Static Methods and Expecting Polymorphic Behavior: Declaring a static method in a subclass with the same signature as a static method in its superclass does NOT achieve true runtime polymorphism — this is called 'method hiding', not overriding. Which static method version gets called is determined by the REFERENCE VARIABLE's declared type at compile time, not the actual object's runtime type, which often surprises developers expecting the same dynamic dispatch behavior seen with instance methods.
  • Ambiguous Overloaded Calls with Autoboxing and Varargs: When multiple overloaded methods could all technically match a given call due to Java's autoboxing (int to Integer) or widening rules, the compiler may report an 'ambiguous method call' error if it cannot definitively determine the single best match, particularly when mixing overloads that use primitive types, their wrapper classes, and varargs parameters together.
  • Always Use @Override When Overriding Methods: Consistently annotate every intended method override with '@Override', allowing the compiler to catch mismatched signatures immediately as a compile-time error, preventing an accidental new overload from silently being created instead of a true override.
  • Design Overloaded Methods with Clearly Distinct Purposes: When creating overloaded methods, ensure each variant has a genuinely distinct, intuitive purpose based on its parameters (e.g., different units, different levels of detail) rather than overloading purely for the sake of it, which can confuse callers about which specific overload will actually be invoked for a given call.
  • Leverage Polymorphism to Avoid Type-Checking Chains: When you find yourself writing a long if-else or switch chain checking 'instanceof' or a type field to decide what action to take, this is usually a strong signal that runtime polymorphism (via method overriding) should be used instead, replacing the type-checking logic with a single polymorphic method call that lets each object's own overridden method handle its specific behavior.
  • Avoid Overloading with Ambiguous Numeric Type Combinations: Be cautious when overloading methods with parameter types that could both satisfy a call through Java's automatic widening or autoboxing rules (e.g., avoid having both an 'int, long' and 'long, int' overload if callers frequently pass two int arguments), since this significantly increases the risk of confusing 'ambiguous method call' compile errors for callers of your API.
What is dynamic method dispatch, and how does it enable runtime polymorphism in Java?
Dynamic method dispatch is the mechanism by which the Java Virtual Machine (JVM) determines, at runtime, which specific overridden version of a method to execute, based on the ACTUAL type of the object a reference variable points to, rather than the reference variable's own DECLARED type. For example, if a variable declared as type 'Animal' actually holds a 'Dog' object, calling an overridden method on that variable executes Dog's specific version, not Animal's, because the JVM looks up the method in the object's actual runtime class hierarchy at the moment of the call, not based on the compile-time type of the reference. This mechanism is precisely what enables runtime polymorphism, since the same line of calling code can produce different behavior depending on which actual object type is being referenced at that moment during execution.
Can you overload a method by changing only its return type, keeping the same name and parameter list? Explain why or why not.
No, this is not allowed. Java's method overloading resolution is based entirely on a method's 'signature', which consists of its name plus its parameter list (the number, types, and order of parameters) — the return type is explicitly NOT part of a method's signature for overload resolution purposes. If two methods share the exact same name and parameter list but differ only in return type, the compiler considers this a duplicate method definition and raises a compile-time error, since it would be genuinely ambiguous which method to call in a context where the return value isn't used or assigned to a specific type (e.g., simply calling 'getValue();' as a standalone statement, ignoring any return value).
Why is overriding a static method in Java referred to as 'method hiding' rather than true polymorphic overriding?
Static methods belong to the class itself, not to any specific instance/object of that class, and are therefore resolved at COMPILE TIME based on the reference variable's declared type, not the object's actual runtime type — this is called static binding. When a subclass defines a static method with the same signature as one in its superclass, it doesn't participate in dynamic method dispatch at all; instead, it simply 'hides' the superclass's static method for calls made through a subclass-typed reference, while calls made through a superclass-typed reference (even if it holds a subclass object) will still resolve to the superclass's static method version. This fundamentally differs from true (runtime) polymorphic overriding of instance methods, where the object's actual type — not the reference's declared type — always determines which version executes.
Create a 'PaymentMethod' superclass with a 'processPayment(double amount)' method, and two subclasses 'CreditCard' and 'UPI' that override it with their own messages. Demonstrate runtime polymorphism by looping through an array of PaymentMethod objects.
class PaymentMethod { public void processPayment(double amount) { System.out.println("Processing generic payment of: " + amount); } } class CreditCard extends PaymentMethod { @Override public void processPayment(double amount) { System.out.println("Processing credit card payment of: " + amount); } } class UPI extends PaymentMethod { @Override public void processPayment(double amount) { System.out.println("Processing UPI payment of: " + amount); } } public class PaymentDemo { public static void main(String[] args) { PaymentMethod[] payments = { new CreditCard(), new UPI() }; for (PaymentMethod p : payments) { p.processPayment(1500.0); } } } // Output: // Processing credit card payment of: 1500.0 // Processing UPI payment of: 1500.0
What will be the output of the following code, and explain the reasoning based on static method hiding? class Parent { static void show() { System.out.println("Parent's static show()"); } } class Child extends Parent { static void show() { System.out.println("Child's static show()"); } } public class StaticHidingDemo { public static void main(String[] args) { Parent p = new Child(); p.show(); } }
The output will be: 'Parent's static show()'. Even though the object created is actually a 'Child' instance, the reference variable 'p' is declared with type 'Parent'. Since 'show()' is a STATIC method, its resolution happens at COMPILE TIME based on the reference variable's declared type (Parent), not the object's actual runtime type (Child) — this is static method hiding, not true polymorphic overriding. If 'show()' were an instance (non-static) method instead, the output would be 'Child's static show()' (well, without the 'static' modifier), since instance methods use dynamic dispatch based on the actual object type.
Write an overloaded 'display' method with three versions: one accepting a single String, one accepting a String and an int, and one accepting two Strings. Call all three from main.
public class OverloadPractice { static void display(String s) { System.out.println("String: " + s); } static void display(String s, int num) { System.out.println("String and int: " + s + ", " + num); } static void display(String s1, String s2) { System.out.println("Two strings: " + s1 + ", " + s2); } public static void main(String[] args) { display("Hello"); display("Count", 5); display("First", "Second"); } } // Output: // String: Hello // String and int: Count, 5 // Two strings: First, Second

Polymorphism allows a single method name or reference type to exhibit different behaviors depending on context, implemented in Java through compile-time polymorphism (method overloading, resolved by the compiler based on parameter lists) and runtime polymorphism (method overriding, resolved dynamically at runtime based on an object's actual type via dynamic method dispatch). Understanding the critical distinction between these two forms — including the important exception that static methods use compile-time 'hiding' rather than true runtime overriding — is essential for writing flexible, extensible object-oriented Java code that avoids repetitive type-checking logic.

© 2026 CompileX. Maintained by Aditya Kumar Sharma.