Hook: Polymorphism is like one universal remote controlling a TV, speaker, and AC: the button is the same, but each device responds in its own way.
Question: Explain polymorphism in Java.
Answer: Polymorphism means one thing can take many forms. In Java, it usually means a parent type can refer to different child objects, and when you call an overridden method, Java runs the child version at runtime. This makes code flexible because you can add new classes without rewriting the code that uses them.
Interview-Ready Answer: I think of polymorphism as “one interface, many behaviors.” In Java, I can store different subclasses in a parent reference or interface reference, and when I call an overridden method, the JVM picks the actual object’s version at runtime. That is why a PaymentMethod variable can represent card, UPI, or wallet payments without the caller knowing the concrete class.
Polymorphism is a core OOP idea where code talks to a general type, but the actual object decides what behavior happens. The word sounds fancy, but the mental model is simple: same call, different result.
interface or an abstract class (a class that cannot be created directly and is meant to be extended).override (replace inherited behavior with a new one).PaymentMethod or Shape.Interviewers often expect you to know that Java supports polymorphism in two ways:
| Aspect | Overloading | Overriding |
|---|---|---|
| When decided | Compile time | Runtime |
| Method shape | Same name, different params | Same signature, new body |
| Inheritance needed | No | Yes |
| Typical use | Convenience methods | Flexible behavior |
Compile-time polymorphism is usually method overloading. The compiler chooses the method based on the parameter list. Runtime polymorphism is method overriding. The JVM chooses the implementation based on the real object.
CardPayment, but store it in a parent reference like PaymentMethod method = new CardPayment(...).Use polymorphism when you want to write code against a stable contract instead of a concrete class. That is ideal for payments, notifications, report generation, rendering, logging, and plugin systems.
null or upcasting.A strong answer says: polymorphism is about calling a method through a parent type and getting child-specific behavior at runtime. A stronger answer adds the difference between overloading and overriding, plus one limitation such as static or final methods.
Imagine an e-commerce checkout service with multiple payment types: card, UPI, and wallet. The service keeps a List<PaymentMethod> and calls pay() on each item, so the checkout flow does not care which payment type is used.
Now picture a production bug: a new WalletPayment class was added, but the team accidentally copied an old pattern with a base class method that did nothing. Orders reached the UI as “paid,” but no money was captured. Symptoms included reconciliation mismatches, customer support tickets, and logs like Payment failed: Insufficient wallet balance or, worse, no gateway call at all because the wrong method was being used.
This is exactly why polymorphism matters: the checkout service should depend on the contract, not on a giant if/else chain. If the implementation is correct, adding a new payment type means adding a new class, not rewriting the checkout logic.
import java.util.Arrays;
import java.util.List;
public class PolymorphismDemo {
// The caller works with the interface, not the concrete classes.
// This is the heart of runtime polymorphism.
interface PaymentMethod {
void pay();
}
static abstract class AbstractPayment implements PaymentMethod {
protected final String orderId;
protected final double amount;
protected AbstractPayment(String orderId, double amount) {
if (orderId == null || orderId.isBlank()) {
throw new IllegalArgumentException("orderId cannot be blank");
}
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive");
}
this.orderId = orderId;
this.amount = amount;
}
protected String formatAmount(double value) {
return String.format(java.util.Locale.US, "$.2f", value).replace("$.2f", String.format(java.util.Locale.US, "%.2f", value));
}
}
static class CardPayment extends AbstractPayment {
private final String last4;
CardPayment(String orderId, double amount, String last4) {
super(orderId, amount);
if (last4 == null || last4.length() != 4 || !last4.chars().allMatch(Character::isDigit)) {
throw new IllegalArgumentException("Card last4 must be exactly 4 digits");
}
this.last4 = last4;
}
@Override
public void pay() {
System.out.println("CARD " + orderId + ": charged $" + String.format(java.util.Locale.US, "%.2f", amount) + " using ****" + last4);
}
}
static class UpiPayment extends AbstractPayment {
private final String vpa;
UpiPayment(String orderId, double amount, String vpa) {
super(orderId, amount);
if (vpa == null || vpa.isBlank()) {
throw new IllegalArgumentException("UPI ID cannot be blank");
}
this.vpa = vpa;
}
@Override
public void pay() {
System.out.println("UPI " + orderId + ": collected $" + String.format(java.util.Locale.US, "%.2f", amount) + " from " + vpa);
}
}
static class WalletPayment extends AbstractPayment {
private double balance;
WalletPayment(String orderId, double amount, double balance) {
super(orderId, amount);
if (balance < 0) {
throw new IllegalArgumentException("balance cannot be negative");
}
this.balance = balance;
}
@Override
public void pay() {
if (balance < amount) {
throw new IllegalStateException(
"Insufficient wallet balance. Have $" + String.format(java.util.Locale.US, "%.2f", balance)
+ ", need $" + String.format(java.util.Locale.US, "%.2f", amount));
}
balance -= amount;
System.out.println("WALLET " + orderId + ": debited $" + String.format(java.util.Locale.US, "%.2f", amount)
+ ", remaining $" + String.format(java.util.Locale.US, "%.2f", balance));
}
}
static void processPayments(List<PaymentMethod> methods) {
for (PaymentMethod method : methods) {
if (method == null) {
// Edge case: the collection may contain a bad value. Polymorphism does not remove validation.
System.out.println("Skipping null payment method");
continue;
}
try {
// The same call name, but the JVM selects the concrete implementation at runtime.
method.pay();
} catch (RuntimeException ex) {
System.out.println("Payment failed: " + ex.getMessage());
}
}
}
public static void main(String[] args) {
List<PaymentMethod> methods = Arrays.asList(
new CardPayment("ORD-1001", 49.99, "1234"),
new UpiPayment("ORD-1002", 19.50, "alice@upi"),
new WalletPayment("ORD-1003", 75.00, 20.00), // failure path: not enough balance
null // edge case: caller must still handle invalid collection entries
);
processPayments(methods);
}
}Follow-up & Tricky Questions:
final allow overriding? No. A final method cannot be overridden, so it cannot participate in runtime polymorphism.null and upcasting can surprise people.Common Mistakes:
static, final, and private methods do not override. If a method cannot be overridden, it cannot demonstrate runtime polymorphism.Memory Hook: Think “one remote, many devices”: the same button press goes through one interface, but each device reacts in its own way.
Cheat Sheet:
static, final, and private methods do not override.Practice Tasks:
Shape interface with Circle and Rectangle, then call area() through a List<Shape>.CashPayment, without changing the loop that processes payments.