Java Tutorial
🔍

Java Polymorphism

Java Polymorphism

Polymorphism means one interface, many implementations. In Java, it is what allows a single method call to execute different code depending on the actual type of the object it is called on — without the caller needing to know which type it is dealing with. A list of Payment objects can contain credit card payments, UPI payments, and wallet payments. One loop, one method call, correct behaviour for each type automatically.

This is not just elegant code — it is a structural property that lets you extend a system with new types without modifying the existing code that uses those types. That extensibility is the practical payoff.

What Is Polymorphism in Java?

Polymorphism is the OOP principle that allows a single method name to resolve to different implementations depending on the context. Java has two forms:

  • Compile-time polymorphism — resolved at compile time based on method signatures. Achieved through method overloading.
  • Runtime polymorphism — resolved at runtime based on the actual object type. Achieved through method overriding and dynamic dispatch.

Both forms share the same surface: one method name, multiple behaviours. The difference is when the resolution happens — before the program runs or during execution.

Two Types of Polymorphism in Java

AspectCompile-Time PolymorphismRuntime Polymorphism
Also calledStatic polymorphism / Early bindingDynamic polymorphism / Late binding
Achieved viaMethod overloadingMethod overriding
Resolved byCompiler — at compile timeJVM — at runtime
Decided based onParameter types and countActual object type at runtime
Requires inheritanceNoYes
PerformanceSlightly faster — no lookup neededSlight overhead — dynamic dispatch
FlexibilityLower — fixed at compile timeHigher — behaviour varies at runtime
Exampleprint(int) vs print(String)shape.draw() on Circle vs Rectangle

Compile-Time Polymorphism — Method Overloading

Method overloading is when a class defines multiple methods with the same name but different parameter lists. The compiler chooses which version to call based on the argument types at the call site. This decision is made before the program runs — hence compile-time.

Java
1// File: InvoiceFormatter.java 2 3public class InvoiceFormatter { 4 5 // Overloaded format() — same name, different parameter types 6 public String format(double amount) { 7 return String.format("Rs.%.2f", amount); 8 } 9 10 public String format(double amount, String currency) { 11 return String.format("%s %.2f", currency, amount); 12 } 13 14 public String format(double amount, String currency, boolean includeSymbol) { 15 String symbol = includeSymbol ? "₹" : ""; 16 return String.format("%s%s %.2f", symbol, currency, amount); 17 } 18 19 public String format(int quantity, String unit) { 20 return quantity + " " + unit; 21 } 22}
Java
1// File: OverloadingDemo.java 2 3public class OverloadingDemo { 4 5 public static void main(String[] args) { 6 7 InvoiceFormatter formatter = new InvoiceFormatter(); 8 9 // Compiler picks the correct overload at compile time 10 System.out.println(formatter.format(1499.0)); 11 System.out.println(formatter.format(1499.0, "INR")); 12 System.out.println(formatter.format(1499.0, "INR", true)); 13 System.out.println(formatter.format(5, "units")); 14 } 15}
Output:
Rs.1499.00
INR 1499.00
₹INR 1499.00
5 units

The compiler examines the argument types at the call site and selects the matching overload. If no exact match exists, it applies widening conversions — an int argument can match a double parameter. Overloading is resolved entirely at compile time — the JVM never needs to look up which version to run.

Overloading Rules

RuleDescription
Different parameter countprint(int) and print(int, int) — valid
Different parameter typesprint(int) and print(double) — valid
Different parameter orderprint(int, String) and print(String, int) — valid
Return type aloneint get() and double get() — NOT valid
Access modifier aloneCannot overload by changing only visibility
Varargs ambiguityprint(int...) and print(int) — compiles but may cause ambiguity

Runtime Polymorphism — Method Overriding

Method overriding is when a child class provides its own implementation of a method inherited from the parent. The method signature must match exactly. At runtime, the JVM looks at the actual type of the object — not the reference type — and calls the most specific override. This is called dynamic dispatch.

Java
1// File: Notification.java 2 3public abstract class Notification { 4 5 private final String notificationId; 6 private final String recipientId; 7 protected final String message; 8 9 public Notification(String notificationId, String recipientId, String message) { 10 this.notificationId = notificationId; 11 this.recipientId = recipientId; 12 this.message = message; 13 } 14 15 public String getNotificationId() { return notificationId; } 16 public String getRecipientId() { return recipientId; } 17 18 // Abstract — each subclass must provide its own delivery mechanism 19 public abstract void deliver(); 20 21 // Concrete — shared across all subclasses 22 public String getSummary() { 23 return "[" + getClass().getSimpleName() + "] To: " + recipientId 24 + " | " + message; 25 } 26}
Java
1// File: SmsNotification.java 2 3public class SmsNotification extends Notification { 4 5 private final String phoneNumber; 6 7 public SmsNotification(String id, String recipientId, 8 String phoneNumber, String message) { 9 super(id, recipientId, message); 10 this.phoneNumber = phoneNumber; 11 } 12 13 @Override 14 public void deliver() { 15 System.out.println("SMS → " + phoneNumber + ": " + message); 16 } 17}
Java
1// File: EmailNotification.java 2 3public class EmailNotification extends Notification { 4 5 private final String emailAddress; 6 private final String subject; 7 8 public EmailNotification(String id, String recipientId, 9 String emailAddress, String subject, String body) { 10 super(id, recipientId, body); 11 this.emailAddress = emailAddress; 12 this.subject = subject; 13 } 14 15 @Override 16 public void deliver() { 17 System.out.println("Email → " + emailAddress 18 + " | Subject: " + subject 19 + " | Body: " + message); 20 } 21}
Java
1// File: PushNotification.java 2 3public class PushNotification extends Notification { 4 5 private final String deviceToken; 6 7 public PushNotification(String id, String recipientId, 8 String deviceToken, String message) { 9 super(id, recipientId, message); 10 this.deviceToken = deviceToken; 11 } 12 13 @Override 14 public void deliver() { 15 System.out.println("Push → Device: " + deviceToken + " | " + message); 16 } 17}
Java
1// File: RuntimePolymorphismDemo.java 2 3import java.util.List; 4 5public class RuntimePolymorphismDemo { 6 7 public static void main(String[] args) { 8 9 // All referenced as Notification — actual type determines behaviour 10 List<Notification> notifications = List.of( 11 new SmsNotification("N001", "USER-001", 12 "9876543210", "Your OTP is 847291."), 13 new EmailNotification("N002", "USER-002", 14 "priya@example.com", "Order Shipped", 15 "Your order ORD-4521 is on its way."), 16 new PushNotification("N003", "USER-003", 17 "device-token-xyz", "Flash sale starts in 10 minutes!") 18 ); 19 20 System.out.println("=== Sending Notifications ===\n"); 21 for (Notification notification : notifications) { 22 notification.deliver(); // JVM calls the correct override at runtime 23 System.out.println(" " + notification.getSummary()); 24 System.out.println(); 25 } 26 } 27}
Output:
=== Sending Notifications ===

SMS  → 9876543210: Your OTP is 847291.
  [SmsNotification] To: USER-001 | Your OTP is 847291.

Email → priya@example.com | Subject: Order Shipped | Body: Your order ORD-4521 is on its way.
  [EmailNotification] To: USER-002 | Your order ORD-4521 is on its way.

Push → Device: device-token-xyz | Flash sale starts in 10 minutes!
  [PushNotification] To: USER-003 | Flash sale starts in 10 minutes!

The loop calls notification.deliver() on a Notification reference. The JVM looks at the actual object type — SmsNotification, EmailNotification, PushNotification — and dispatches to the correct deliver() implementation. The calling code is completely insulated from which type it is working with. Adding a WhatsAppNotification type requires no changes to this loop.

How Dynamic Dispatch Works Internally

When the JVM resolves a virtual method call, it uses the vtable (virtual method table) — a data structure maintained per class that maps method signatures to their implementations.

Reference type declared:    Notification
Actual object type:         SmsNotification

Step 1: JVM reads the method name: deliver()
Step 2: JVM looks at the actual object's class: SmsNotification
Step 3: JVM checks SmsNotification's vtable — finds deliver() override
Step 4: JVM executes SmsNotification.deliver()

Result: SmsNotification.deliver() runs — NOT Notification.deliver()

This lookup happens at runtime for every virtual method call. It is why runtime polymorphism has a slight overhead compared to compile-time resolution — but this overhead is negligible in practice because modern JVMs inline frequently called virtual methods.

Upcasting and Downcasting

Upcasting is assigning a child type object to a parent type reference. It happens implicitly and is always safe.

Downcasting is reassigning a parent type reference back to a child type. It must be explicit and can fail at runtime with ClassCastException if the actual object is not the expected type.

Java
1// File: CastingDemo.java 2 3public class CastingDemo { 4 5 public static void main(String[] args) { 6 7 // Upcasting — implicit, always safe 8 Notification notification = new SmsNotification( 9 "N010", "USER-010", "9999999999", "Test message" 10 ); 11 12 // notification.phoneNumber — compile error: Notification reference cannot 13 // see SmsNotification-specific members 14 15 // Downcasting — explicit, must verify first 16 if (notification instanceof SmsNotification smsNotification) { 17 // Java 16+ pattern matching instanceof — cleaner than cast then access 18 System.out.println("Phone: " + smsNotification.getRecipientId()); 19 } 20 21 // Unsafe downcast — runtime ClassCastException 22 try { 23 EmailNotification email = (EmailNotification) notification; 24 } catch (ClassCastException ex) { 25 System.out.println("ClassCastException: cannot cast SmsNotification to EmailNotification"); 26 } 27 28 // Safe classical downcast with instanceof check 29 if (notification instanceof SmsNotification) { 30 SmsNotification sms = (SmsNotification) notification; 31 System.out.println("Classic cast succeeded for SmsNotification."); 32 } 33 } 34}
Output:
Phone: USER-010
ClassCastException: cannot cast SmsNotification to EmailNotification
Classic cast succeeded for SmsNotification.

Upcasting vs Downcasting

AspectUpcastingDowncasting
DirectionChild → Parent referenceParent reference → Child type
SyntaxImplicit — no cast neededExplicit — (ChildType) ref
SafetyAlways safeCan throw ClassCastException
AccessOnly parent members visibleChild-specific members accessible
When neededPolymorphic collections, method parametersAccessing child-specific behaviour
Best practiceCommon and recommendedAlways use instanceof check first
Java 16+ patternNot applicableif (obj instanceof Child c) — pattern matching

Method Overriding vs Method Overloading

AspectMethod OverridingMethod Overloading
DefinitionRedefining a parent method in a child classDefining multiple methods with same name, different parameters
Resolved atRuntime — dynamic dispatchCompile time — static dispatch
Inheritance requiredYesNo
SignatureSame name, same parametersSame name, different parameters
Return typeSame or covariant subtypeCan differ
Access modifierSame or widerCan differ
@OverrideRecommended — compiler validatesNot applicable
Polymorphism typeRuntimeCompile-time
static methodsCannot be overridden (hidden instead)Can be overloaded
private methodsCannot be overriddenCan be overloaded

Polymorphism vs Inheritance

AspectInheritancePolymorphism
What it isMechanism — child acquires parent membersPrinciple — one interface, multiple behaviours
Primary purposeCode reuseFlexible, extensible behaviour
Requiresextends keywordInheritance (for runtime) or overloading (for compile-time)
RelationshipIS-A — structuralBehavioural — how methods resolve
Without inheritanceN/A — inheritance is the mechanismCompile-time polymorphism works without it
Key conceptParent provides members to childMethod call resolves to correct implementation
EnablesPolymorphism (runtime form)Open-Closed Principle

Real-World Example — Payment Processing System

The Business Problem

You are building the payment gateway integration layer for a fintech platform — similar to what Razorpay or PayU runs for its multi-gateway checkout. The system must support UPI, card, and wallet payments. Each payment type has different processing logic. The calling code — the checkout service — must work with all types through a single interface without changing when new payment methods are added.

Implementation

Java
1// File: PaymentConfig.java 2 3public final class PaymentConfig { 4 5 public static final double CARD_PROCESSING_FEE = 0.02; // 2% 6 public static final double WALLET_CASHBACK_RATE = 0.05; // 5% 7 8 private PaymentConfig() {} 9}
Java
1// File: Payment.java 2 3public abstract class Payment { 4 5 private final String paymentId; 6 private final double amount; 7 private final String orderId; 8 9 public Payment(String paymentId, double amount, String orderId) { 10 if (amount <= 0) throw new IllegalArgumentException("Amount must be positive."); 11 this.paymentId = paymentId; 12 this.amount = amount; 13 this.orderId = orderId; 14 } 15 16 public String getPaymentId() { return paymentId; } 17 public double getAmount() { return amount; } 18 public String getOrderId() { return orderId; } 19 20 // Abstract — each payment type implements its own processing 21 public abstract boolean process(); 22 23 // Abstract — each type computes its own effective amount 24 public abstract double getEffectiveAmount(); 25 26 // Concrete — shared receipt format 27 public String getReceipt() { 28 return paymentId + " | Order: " + orderId 29 + " | Amount: Rs." + String.format("%.2f", amount) 30 + " | Effective: Rs." + String.format("%.2f", getEffectiveAmount()) 31 + " | Status: " + (process() ? "SUCCESS" : "FAILED"); 32 } 33}
Java
1// File: UpiPayment.java 2 3public class UpiPayment extends Payment { 4 5 private final String upiId; 6 7 public UpiPayment(String paymentId, double amount, 8 String orderId, String upiId) { 9 super(paymentId, amount, orderId); 10 if (!upiId.contains("@")) { 11 throw new IllegalArgumentException("Invalid UPI ID: " + upiId); 12 } 13 this.upiId = upiId; 14 } 15 16 @Override 17 public boolean process() { 18 System.out.println(" Processing UPI payment from " + upiId 19 + " — Rs." + getAmount()); 20 return true; 21 } 22 23 @Override 24 public double getEffectiveAmount() { 25 return getAmount(); // UPI has no extra fees 26 } 27}
Java
1// File: CardPayment.java 2 3public class CardPayment extends Payment { 4 5 private final String maskedCardNumber; 6 private final String cardNetwork; 7 8 public CardPayment(String paymentId, double amount, String orderId, 9 String maskedCardNumber, String cardNetwork) { 10 super(paymentId, amount, orderId); 11 this.maskedCardNumber = maskedCardNumber; 12 this.cardNetwork = cardNetwork; 13 } 14 15 @Override 16 public boolean process() { 17 double fee = getAmount() * PaymentConfig.CARD_PROCESSING_FEE; 18 System.out.println(" Processing " + cardNetwork + " card " 19 + maskedCardNumber + " — Rs." + getAmount() 20 + " + Rs." + String.format("%.2f", fee) + " fee"); 21 return true; 22 } 23 24 @Override 25 public double getEffectiveAmount() { 26 return getAmount() * (1 + PaymentConfig.CARD_PROCESSING_FEE); 27 } 28}
Java
1// File: WalletPayment.java 2 3public class WalletPayment extends Payment { 4 5 private final String walletProvider; 6 private final double walletBalance; 7 8 public WalletPayment(String paymentId, double amount, String orderId, 9 String walletProvider, double walletBalance) { 10 super(paymentId, amount, orderId); 11 this.walletProvider = walletProvider; 12 this.walletBalance = walletBalance; 13 } 14 15 @Override 16 public boolean process() { 17 if (walletBalance < getAmount()) { 18 System.out.println(" Wallet payment FAILED — insufficient balance in " 19 + walletProvider); 20 return false; 21 } 22 double cashback = getAmount() * PaymentConfig.WALLET_CASHBACK_RATE; 23 System.out.println(" Processing " + walletProvider + " wallet — Rs." 24 + getAmount() + " | Cashback: Rs." 25 + String.format("%.2f", cashback)); 26 return true; 27 } 28 29 @Override 30 public double getEffectiveAmount() { 31 double cashback = getAmount() * PaymentConfig.WALLET_CASHBACK_RATE; 32 return getAmount() - cashback; 33 } 34}
Java
1// File: CheckoutService.java 2 3import java.util.List; 4 5public class CheckoutService { 6 7 // Works with any Payment subtype — polymorphism in action 8 public void processPayments(List<Payment> payments) { 9 System.out.println("=== Processing Payments ===\n"); 10 double totalCollected = 0; 11 12 for (Payment payment : payments) { 13 payment.process(); // runtime dispatch — correct type called 14 totalCollected += payment.getEffectiveAmount(); 15 System.out.println(" Receipt: " + payment.getPaymentId() 16 + " | Effective: Rs." 17 + String.format("%.2f", payment.getEffectiveAmount())); 18 System.out.println(); 19 } 20 System.out.printf("Total collected: Rs.%.2f%n", totalCollected); 21 } 22}
Java
1// File: PaymentDemo.java 2 3import java.util.List; 4 5public class PaymentDemo { 6 7 public static void main(String[] args) { 8 9 List<Payment> payments = List.of( 10 new UpiPayment("PAY-001", 1499.0, "ORD-101", "rahul@upi"), 11 new CardPayment("PAY-002", 3999.0, "ORD-102", 12 "**** **** **** 4521", "VISA"), 13 new WalletPayment("PAY-003", 799.0, "ORD-103", 14 "PhonePe", 1500.0), 15 new WalletPayment("PAY-004", 5000.0, "ORD-104", 16 "Paytm", 3000.0) // will fail — insufficient balance 17 ); 18 19 CheckoutService checkout = new CheckoutService(); 20 checkout.processPayments(payments); 21 } 22}
Output:
=== Processing Payments ===

  Processing UPI payment from rahul@upi — Rs.1499.0
  Receipt: PAY-001 | Effective: Rs.1499.00

  Processing VISA card **** **** **** 4521 — Rs.3999.0 + Rs.79.98 fee
  Receipt: PAY-002 | Effective: Rs.4078.98

  Processing PhonePe wallet — Rs.799.0 | Cashback: Rs.39.95
  Receipt: PAY-003 | Effective: Rs.759.05

  Wallet payment FAILED — insufficient balance in Paytm
  Receipt: PAY-004 | Effective: Rs.4750.00

Total collected: Rs.11087.03

CheckoutService.processPayments() does not contain a single instanceof check or if-else for payment types. It calls payment.process() and payment.getEffectiveAmount() — the JVM dispatches to the correct implementation for each type at runtime. Adding a new CryptoPayment type requires writing one new class — no changes to CheckoutService. This is the open-closed principle delivered by polymorphism.

Best Practices

Program to interfaces and abstract types, not concrete classes

When a method parameter or field type is declared as Payment instead of UpiPayment, the code works correctly with any current or future payment type. The calling code is decoupled from the implementation. Teams following clean architecture consistently write List<Payment> rather than List<UpiPayment> when the collection will hold multiple types.

Use @Override on every overriding method

@Override tells the compiler to verify that the method actually overrides something in the parent. Without it, a typo — delivar() instead of deliver() — silently creates a new method rather than overriding, and the dispatch never reaches the child's code. The bug compiles cleanly and only appears at runtime. Every override in production Java must have @Override.

Use instanceof with pattern matching before downcasting

When a downcast is genuinely necessary, use Java 16+ pattern matching instanceof: if (notification instanceof SmsNotification sms). This eliminates both the explicit cast and the possibility of ClassCastException in a single construct. A naked cast without instanceof is consistently flagged in code review.

Avoid long instanceof chains — they signal a missing override

A series of if (obj instanceof A) ... else if (obj instanceof B) ... is usually a sign that a polymorphic method should have been used instead. Each branch is doing what an override would do — but in the caller rather than the class. Move the behaviour into the classes themselves and call it through a common interface.

Common Mistakes

Mistake 1 — Thinking Static Methods Are Polymorphic

Java
1public class Animal { 2 public static String sound() { return "..."; } 3 public String describe() { return "I am an Animal"; } 4} 5 6public class Dog extends Animal { 7 public static String sound() { return "Woof"; } // hides Animal.sound() 8 @Override 9 public String describe() { return "I am a Dog"; } 10} 11 12public class StaticPolymorphismDemo { 13 public static void main(String[] args) { 14 Animal ref = new Dog(); 15 System.out.println(ref.sound()); // prints "..." — reference type wins 16 System.out.println(ref.describe()); // prints "I am a Dog" — runtime type wins 17 } 18}
Output:
...
I am a Dog

Static methods use method hiding, not overriding. The reference type (Animal) determines which static method runs — not the actual object type. Only instance methods participate in dynamic dispatch and runtime polymorphism.

Mistake 2 — Overloading and Overriding Confusion

Java
1public class Printer { 2 public void print(Object obj) { 3 System.out.println("Printing Object: " + obj); 4 } 5} 6 7public class SmartPrinter extends Printer { 8 // This is OVERLOADING — not overriding — different parameter type 9 public void print(String text) { 10 System.out.println("Printing String: " + text); 11 } 12} 13 14public class OverloadConfusionDemo { 15 public static void main(String[] args) { 16 Printer printer = new SmartPrinter(); 17 printer.print("Hello"); // calls Printer.print(Object) — NOT SmartPrinter.print(String) 18 // Because reference type is Printer, compiler picks print(Object) 19 // SmartPrinter.print(String) is overloading, not overriding 20 } 21}
Output:
Printing Object: Hello

SmartPrinter.print(String) does not override Printer.print(Object) — it overloads with a different parameter type. Since the reference type is Printer, the compiler selects print(Object). Adding @Override would have caught this at compile time — the compiler would have reported that no method with print(String) exists in the parent to override.

Mistake 3 — Calling Overridable Methods in Constructors

Java
1public class Base { 2 public Base() { 3 display(); // runtime dispatch sends this to Child.display() 4 } 5 public void display() { System.out.println("Base display"); } 6} 7 8public class Child extends Base { 9 private final String name; 10 public Child(String name) { 11 super(); // triggers Base() → calls display() before name is set 12 this.name = name; 13 } 14 @Override 15 public void display() { 16 System.out.println("Child display: " + name); // name is null here 17 } 18}
Java
1public class ConstructorPolymorphismDemo { 2 public static void main(String[] args) { 3 Child child = new Child("Alice"); 4 child.display(); // correct output 5 } 6}
Output:
Child display: null
Child display: Alice

When Base() calls display(), the runtime type is already Child — so Child.display() runs. But Child's constructor body has not executed yet — name is null. The first output is incorrect. Never call overridable methods from constructors.

Interview Questions

Q1. What is polymorphism in Java and what are its two types?

Polymorphism means one method name can resolve to different implementations depending on the context. Java has compile-time polymorphism — achieved through method overloading, resolved by the compiler based on argument types at the call site — and runtime polymorphism — achieved through method overriding, resolved by the JVM at runtime based on the actual object type using dynamic dispatch. Both allow one interface to represent many behaviours, but at different phases of program execution.

Q2. What is the difference between method overloading and method overriding?

Overloading is defining multiple methods in the same class with the same name but different parameter lists — resolved at compile time, does not require inheritance. Overriding is a child class providing its own implementation of an inherited parent method with the exact same signature — resolved at runtime through dynamic dispatch, requires inheritance. Overloading is compile-time polymorphism; overriding is runtime polymorphism. @Override applies only to overriding. Static methods can be overloaded but not overridden.

Q3. What is dynamic dispatch in Java?

Dynamic dispatch is the mechanism by which the JVM resolves virtual method calls at runtime. When a method is called through a parent-type reference, the JVM looks at the actual runtime type of the object — not the declared reference type — and calls the most specific override found in that type's vtable. This is what makes runtime polymorphism work: the same method call on a Notification reference routes to SmsNotification.deliver(), EmailNotification.deliver(), or PushNotification.deliver() depending on the actual object, without the caller needing to know which type it holds.

Q4. What is upcasting and downcasting in Java?

Upcasting is assigning a child type object to a parent type reference — implicit, always safe, and necessary for polymorphic collections. Downcasting is assigning a parent type reference back to a child type — must be explicit with a cast operator, and can throw ClassCastException at runtime if the actual object is not the expected type. Always use instanceof before downcasting, or use Java 16+ pattern matching: if (ref instanceof ChildType c) which combines the check and cast safely.

Q5. Can a static method be polymorphic in Java?

No. Static methods use method hiding rather than method overriding. When a child class declares a static method with the same signature as a parent static method, it hides it rather than overriding it. The method that is called is determined by the reference type at compile time, not the actual object type at runtime. Dynamic dispatch — the mechanism of runtime polymorphism — does not apply to static methods. Calling a static method through a parent-type reference always invokes the parent's version.

Q6. What is the open-closed principle and how does polymorphism enable it?

The open-closed principle states that software should be open for extension but closed for modification. Polymorphism enables this by allowing new types to be added — new Payment subtypes, new Notification subtypes — without modifying the code that uses the base type. CheckoutService.processPayments() works correctly with a CryptoPayment type that did not exist when it was written. The calling code is closed for modification — it does not need to change. The system is open for extension — new types implement the required interface. This is the direct practical benefit of runtime polymorphism.

FAQs

What is polymorphism in Java in simple terms?

Polymorphism means the same method call can do different things depending on which object it is called on. A deliver() call on a list of Notification objects sends each one through the correct channel — SMS, email, or push — without the calling code knowing which type each notification is. One method name, many behaviours.

What is the difference between compile-time and runtime polymorphism?

Compile-time polymorphism is resolved before the program runs — the compiler picks the correct overloaded method based on argument types. Runtime polymorphism is resolved during execution — the JVM picks the correct overriding method based on the actual object type at that moment. Overloading is compile-time; overriding is runtime.

Does polymorphism require inheritance in Java?

Runtime polymorphism (overriding) requires inheritance — a child class must extend a parent to override its methods. Compile-time polymorphism (overloading) does not require inheritance — multiple methods with the same name and different parameters can exist in the same class independently of any hierarchy.

What is method hiding in Java?

Method hiding occurs when a child class declares a static method with the same signature as a parent static method. Unlike overriding, it does not participate in dynamic dispatch — the method called depends on the reference type, not the object type. Static methods use compile-time binding; instance methods use dynamic binding. Method hiding is not polymorphism in the runtime sense.

What happens when you call an overridden method through a parent reference?

The JVM uses dynamic dispatch — it looks at the actual type of the object the reference points to and calls the most specific override in that class's method table. The declared reference type determines what methods are visible at compile time; the actual object type determines which implementation runs at runtime.

Why is polymorphism important in Java?

Polymorphism enables the open-closed principle — you can add new types without modifying existing code that works with the base type. It reduces conditional branching (if instanceof A ... else if instanceof B) by moving behaviour into the classes themselves. It makes code more readable, testable, and extensible. Every major Java framework — Spring, Hibernate, Jakarta EE — uses polymorphism as a core architectural mechanism.

Summary

Polymorphism is the principle that one method name resolves to different implementations depending on context. Compile-time polymorphism through overloading gives the same method name multiple signatures — the compiler picks the right one. Runtime polymorphism through overriding gives child classes their own behaviour — the JVM picks the right one at runtime through dynamic dispatch.

The practical payoff is extensibility without modification. A loop that processes Payment objects works correctly with any new payment type added later — no changes required to the loop. A notification dispatcher handles any channel added in the future. This is what polymorphism exists to deliver.

For interviews, be ready to explain both types with concrete code examples, demonstrate the difference between method hiding (static) and overriding (instance), show upcasting and downcasting with instanceof, and articulate how polymorphism enables the open-closed principle. These points appear consistently across service-based recall questions and product-based design discussions.

What to Read Next

TopicLink
How method overriding rules and @Override work in practiceJava Method Overriding →
How method overloading works with widening and ambiguity rulesJava Method Overloading →
How inheritance provides the class hierarchy that runtime polymorphism needsJava Inheritance →
How abstract classes define the polymorphic contract for subclassesJava Abstract Classes →
How interfaces enable polymorphism across unrelated class hierarchiesJava Interfaces →
Java Polymorphism | DevStackFlow