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
| Aspect | Compile-Time Polymorphism | Runtime Polymorphism |
|---|---|---|
| Also called | Static polymorphism / Early binding | Dynamic polymorphism / Late binding |
| Achieved via | Method overloading | Method overriding |
| Resolved by | Compiler — at compile time | JVM — at runtime |
| Decided based on | Parameter types and count | Actual object type at runtime |
| Requires inheritance | No | Yes |
| Performance | Slightly faster — no lookup needed | Slight overhead — dynamic dispatch |
| Flexibility | Lower — fixed at compile time | Higher — behaviour varies at runtime |
| Example | print(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.
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}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
| Rule | Description |
|---|---|
| Different parameter count | print(int) and print(int, int) — valid |
| Different parameter types | print(int) and print(double) — valid |
| Different parameter order | print(int, String) and print(String, int) — valid |
| Return type alone | int get() and double get() — NOT valid |
| Access modifier alone | Cannot overload by changing only visibility |
| Varargs ambiguity | print(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.
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}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}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}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}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.
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
| Aspect | Upcasting | Downcasting |
|---|---|---|
| Direction | Child → Parent reference | Parent reference → Child type |
| Syntax | Implicit — no cast needed | Explicit — (ChildType) ref |
| Safety | Always safe | Can throw ClassCastException |
| Access | Only parent members visible | Child-specific members accessible |
| When needed | Polymorphic collections, method parameters | Accessing child-specific behaviour |
| Best practice | Common and recommended | Always use instanceof check first |
| Java 16+ pattern | Not applicable | if (obj instanceof Child c) — pattern matching |
Method Overriding vs Method Overloading
| Aspect | Method Overriding | Method Overloading |
|---|---|---|
| Definition | Redefining a parent method in a child class | Defining multiple methods with same name, different parameters |
| Resolved at | Runtime — dynamic dispatch | Compile time — static dispatch |
| Inheritance required | Yes | No |
| Signature | Same name, same parameters | Same name, different parameters |
| Return type | Same or covariant subtype | Can differ |
| Access modifier | Same or wider | Can differ |
| @Override | Recommended — compiler validates | Not applicable |
| Polymorphism type | Runtime | Compile-time |
static methods | Cannot be overridden (hidden instead) | Can be overloaded |
private methods | Cannot be overridden | Can be overloaded |
Polymorphism vs Inheritance
| Aspect | Inheritance | Polymorphism |
|---|---|---|
| What it is | Mechanism — child acquires parent members | Principle — one interface, multiple behaviours |
| Primary purpose | Code reuse | Flexible, extensible behaviour |
| Requires | extends keyword | Inheritance (for runtime) or overloading (for compile-time) |
| Relationship | IS-A — structural | Behavioural — how methods resolve |
| Without inheritance | N/A — inheritance is the mechanism | Compile-time polymorphism works without it |
| Key concept | Parent provides members to child | Method call resolves to correct implementation |
| Enables | Polymorphism (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
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}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}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}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}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}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}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
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
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
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}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
| Topic | Link |
|---|---|
| How method overriding rules and @Override work in practice | Java Method Overriding → |
| How method overloading works with widening and ambiguity rules | Java Method Overloading → |
| How inheritance provides the class hierarchy that runtime polymorphism needs | Java Inheritance → |
| How abstract classes define the polymorphic contract for subclasses | Java Abstract Classes → |
| How interfaces enable polymorphism across unrelated class hierarchies | Java Interfaces → |