Object-Oriented Programming · Lesson 55 of 150
Polymorphism
Learn Java polymorphism: overloading vs overriding, upcasting, and how dynamic method dispatch lets the JVM pick a method from the real object at runtime.
At a busy checkout, the cashier says the same two words to every customer: "Please pay." One customer counts out notes, another taps a card, and a third scans a UPI code on the phone. The cashier does not need a separate script for each kind of payment. The request stays the same, and each payment method answers it in its own way.
That idea has a name in Java: polymorphism. It lets one method call behave differently depending on the object that receives it, or on the arguments you pass. Here you will learn the two kinds of polymorphism, how upcasting makes them possible, and how the JVM decides at runtime which method body to run.
What is Polymorphism?
The word comes from Greek and means "many shapes". In practice you meet it in two forms:
- Compile-time polymorphism, also called static binding. Several methods share a name but differ in their parameter lists. The compiler reads the arguments and locks in the right version before the program runs.
- Runtime polymorphism, also called dynamic binding. A subclass overrides a parent method, and a parent-type variable holds the child object. The JVM checks the real object while the program is running and calls its version.
Why is Polymorphism used?
- One piece of code for many types. A checkout method that accepts a
Paymentworks for cards, UPI and cash without aniffor each one. - Easy extension. Adding a new payment method, such as a gift card, means writing one new class. The checkout code does not change.
- Cleaner APIs. Overloading lets callers use one natural name, such as
charge, with different kinds of input. - Loose coupling. Code depends on the general type, not on concrete classes, so parts can be swapped or tested in isolation.
Real-Life Example
Think of a wall socket. The socket offers one thing: power through the same two holes. Plug in a fan and it spins. Plug in a kettle and it boils water. Plug in a phone charger and the battery fills. The socket has no idea what is plugged in, and it does not need to.
In Java terms, the socket is a parent-type reference, such as Payment p. The appliance is the actual object stored in it, such as a CardPayment. Switching on the power is the method call p.pay(). What happens next depends entirely on the appliance, just as the JVM runs the method of the real object. And because every appliance follows the same plug shape, you can add new ones later without rewiring the house.
Syntax
java// Compile-time: same name, different parameters class Counter { void charge(double amount) { } void charge(double amount, double tip) { } } // Runtime: override + parent reference class Parent { void act() { } } class Child extends Parent { @Override void act() { } } Parent ref = new Child(); // upcasting ref.act(); // Child's act() runs
Line by line:
- The two
chargemethods are overloads. They share a name but take different parameters, and the compiler picks one by looking at the call. Childoverridesact()with the same signature.@Overrideasks the compiler to confirm that a parent method really exists.Parent ref = new Child();is upcasting. A child object is stored in a parent-type variable. It is always safe, so no cast is needed.ref.act()compiles becauseParentdeclaresact(). At runtime the JVM runs the version inChild, because that is the real object.
How Polymorphism Works
Runtime polymorphism rests on a mechanism called dynamic method dispatch. The compiler and the JVM each do one part of the job.
textPayment p = new UpiPayment(); p.pay(500); | v Compiler checks the type Payment: has pay(double)? Yes -> code compiles | v At runtime the JVM reads the real class of p: UpiPayment | v Looks up pay in UpiPayment's method table | v Override found -> run it Not found -> use parent's
The compiler only knows the declared type, Payment, so it checks that Payment has a matching pay method and records the call. It does not decide which body will run. When the line executes, the JVM looks at the object on the heap, sees that it is a UpiPayment, and finds pay in that class's method table. If the class overrides the method, that version runs. If not, the table points to the inherited version from the parent. Overloaded methods skip this lookup, because the compiler already chose the exact method from the argument types.
Example Programs
Example 1: Compile-Time Polymorphism with Overloading
javaclass Checkout { String charge(double amount) { return "Paid " + amount; } String charge(double amount, double tip) { return "Paid " + (amount + tip) + " incl tip"; } String charge(double amount, String coupon) { double off = coupon.equals("SAVE50") ? 50 : 0; return "Paid " + (amount - off) + " with " + coupon; } } public class OverloadCounter { public static void main(String[] args) { Checkout c = new Checkout(); System.out.println(c.charge(400)); System.out.println(c.charge(400, 40)); System.out.println(c.charge(400, "SAVE50")); } }
Output:
textPaid 400.0 Paid 440.0 incl tip Paid 350.0 with SAVE50
The same name, charge, does three jobs. The compiler picks each version from the argument types; the int values are widened to double to fit.
Example 2: Runtime Polymorphism with Upcasting
javaclass Payment { String pay(int amount) { return "Generic payment of " + amount; } } class CardPayment extends Payment { @Override String pay(int amount) { return "Card tapped: Rs " + amount; } } class UpiPayment extends Payment { @Override String pay(int amount) { return "UPI scanned: Rs " + amount; } } class CashPayment extends Payment { @Override String pay(int amount) { return "Cash counted: Rs " + amount; } } public class Till { public static void main(String[] args) { Payment[] queue = { new CardPayment(), new UpiPayment(), new CashPayment() }; int[] bills = {250, 90, 600}; for (int i = 0; i < queue.length; i++) { System.out.println(queue[i].pay(bills[i])); } } }
Output:
textCard tapped: Rs 250 UPI scanned: Rs 90 Cash counted: Rs 600
Every element of the array has the declared type Payment, yet each call runs a different method. The loop never checks which kind of payment it holds.
Example 3: What Is and Is Not Polymorphic
javaclass Wallet { String label = "wallet"; static String kind() { return "static in Wallet"; } String describe() { return "Wallet.describe"; } } class GiftCard extends Wallet { String label = "gift card"; static String kind() { return "static in GiftCard"; } @Override String describe() { return "GiftCard.describe"; } String balance() { return "Balance: Rs 1500"; } } public class BindingCheck { public static void main(String[] args) { Wallet w = new GiftCard(); System.out.println(w.describe()); System.out.println(w.label); System.out.println(w.kind()); if (w instanceof GiftCard) { GiftCard g = (GiftCard) w; System.out.println(g.balance()); } } }
Output:
textGiftCard.describe wallet static in Wallet Balance: Rs 1500
Only the instance method follows the real object. The field is chosen by the declared type, and w.kind() runs the Wallet version because static methods are hidden, not overridden. To call balance(), which Wallet does not declare, you must downcast after an instanceof check.
Compile-Time vs Runtime Polymorphism
| Point | Compile-time | Runtime |
|---|---|---|
| Achieved by | Method overloading | Method overriding |
| Also called | Static binding | Dynamic binding |
| Decided by | The compiler | The JVM |
| Based on | Argument types | Actual object type |
| Inheritance needed | No | Yes |
| Methods involved | Same class, or inherited | Parent and child |
| Works for static | Yes | No, static is hidden |
Common Mistakes
- Expecting fields to be polymorphic. A field is read from the declared type, so
w.labelabove printedwallet. - Calling a child-only method through a parent reference.
w.balance()does not compile, becauseWallethas no such method. - Downcasting blindly. Casting a
CardPaymenttoUpiPaymentthrows aClassCastException. Check withinstanceoffirst. - Changing the parameter type by accident. Writing
pay(long)in the child creates an overload, not an override. Add@Overrideso the compiler catches it. - Assuming static methods override. They are resolved from the reference type, so the parent version may run.
Interview Questions
What is dynamic method dispatch?
Ans:It is the process by which the JVM decides, while the program runs, which overridden method to call. It looks at the actual object behind the reference, not the declared type, and runs that class's version.
Is method overloading an example of polymorphism?
Ans:Yes, it is compile-time polymorphism. One method name has several forms, and the compiler picks the right one from the number and types of arguments.
What is upcasting, and is it safe?
Ans:Upcasting stores a subclass object in a superclass variable, like Payment p = new CardPayment(). It is always safe and needs no cast, but through that variable you can only call methods the superclass declares.
Can static or private methods be overridden?
Ans:No. Static methods are bound to the class and are only hidden by a subclass method with the same signature. Private methods are not visible to the child, so a same-named method there is a new, unrelated method.
Why are fields not polymorphic in Java?
Ans:Field access is resolved at compile time from the declared type. Only instance methods go through dynamic dispatch, which is one more reason to keep fields private and expose them through methods.
Summary
- Polymorphism means one name or one reference type with many forms.
- Compile-time polymorphism comes from overloading, and the compiler picks the method.
- Runtime polymorphism comes from overriding, and the JVM picks the method from the real object.
- Upcasting stores a child in a parent variable and is always safe.
- Fields, static methods and private methods do not take part in dynamic dispatch.
- Use
@Overrideandinstanceofchecks to avoid silent overloads and bad casts.
Related Topics
- Method Overloading: the rules behind compile-time polymorphism.
- Method Overriding: the rules behind runtime polymorphism.
- Inheritance: the parent and child link that upcasting needs.
- Interface: polymorphism without a shared parent class.
Practice Problems
Try each problem on your own first, then compare with the solution.
Easy: Parking Fee Calculator
A parking lot charges Rs 30 per hour. Write a ParkingMeter class with three overloaded fee methods: fee(int hours), fee(int hours, boolean electric) that gives electric cars 20 percent off, and fee(int hours, String pass) that makes the stay free for the pass "MONTHLY". Call each one for 4 hours (electric true, pass "MONTHLY").
Expected output:
textRegular: Rs 120 Electric: Rs 96 Pass MONTHLY: Rs 0
Show answerHide answer
fee(int).javaclass ParkingMeter { int fee(int hours) { return hours * 30; } int fee(int hours, boolean electric) { int base = fee(hours); return electric ? base * 80 / 100 : base; } int fee(int hours, String pass) { return pass.equals("MONTHLY") ? 0 : fee(hours); } } public class ParkingGate { public static void main(String[] args) { ParkingMeter meter = new ParkingMeter(); System.out.println("Regular: Rs " + meter.fee(4)); System.out.println("Electric: Rs " + meter.fee(4, true)); System.out.println("Pass MONTHLY: Rs " + meter.fee(4, "MONTHLY")); } }
Output:
textRegular: Rs 120 Electric: Rs 96 Pass MONTHLY: Rs 0
Medium: Notification Channels
An app sends alerts through several channels. Create a parent class Notifier with a method int send(String msg) that returns the cost in paise. SmsNotifier costs 25 paise per started block of 20 characters and prints SMS: <msg>. EmailNotifier always costs 2 and prints Email: <msg>. PushNotifier costs nothing and prints Push: <msg>. Store one of each in a Notifier[], send "Order shipped" through all of them, and print the total cost.
Expected output:
textSMS: Order shipped Email: Order shipped Push: Order shipped Total cost: 27 paise
Show answerHide answer
send, and dynamic method dispatch runs the right one for every element of the parent-type array, so the total is 25 + 2 + 0.javaclass Notifier { int send(String msg) { System.out.println("Log: " + msg); return 0; } } class SmsNotifier extends Notifier { @Override int send(String msg) { System.out.println("SMS: " + msg); int blocks = (msg.length() + 19) / 20; return blocks * 25; } } class EmailNotifier extends Notifier { @Override int send(String msg) { System.out.println("Email: " + msg); return 2; } } class PushNotifier extends Notifier { @Override int send(String msg) { System.out.println("Push: " + msg); return 0; } } public class AlertCenter { public static void main(String[] args) { Notifier[] channels = { new SmsNotifier(), new EmailNotifier(), new PushNotifier() }; int total = 0; for (Notifier n : channels) { total += n.send("Order shipped"); } System.out.println("Total cost: " + total + " paise"); } }
Output:
textSMS: Order shipped Email: Order shipped Push: Order shipped Total cost: 27 paise