Polymorphism in Java
-
Last Updated: September 3, 2025
-
By: javahandson
-
Series
Learn Java in a easy way
Polymorphism in Java lets one method name take many forms. This guide covers method overloading and method overriding. It explains early and late binding, upcasting, and dynamic method dispatch. It also covers interfaces, abstract classes, the traps that catch beginners, and ten interview questions.
Polymorphism in Java is the pillar of object-oriented programming that most people meet last and understand slowest. The word itself sounds heavy. The idea behind it is refreshingly simple.
Poly means many. Morph means form. So polymorphism just means “many forms”. In Java it lets one method name do several different jobs, and it lets one reference variable drive several different objects.
Why should you care? Because polymorphism is what stops your code turning into a swamp of if-else checks. Write your logic once against a general type, and every new subclass slots in for free.
Polymorphism lets a single action behave differently depending on what it acts upon. The caller writes one line. The object decides what that line actually does.
Think about the power button on a universal remote. You press the same button every time. Point it at a TV and the screen wakes up. Point it at a music system and the speakers come alive.
One button, many outcomes. The remote never asks which device it faces. The device answers in its own way.
Java works the same way. You call teach() on a Teacher reference, and the object behind that reference decides whether algebra or physics comes out.
Java gives us two very different ways to reach that goal.
Both wear the label “polymorphism”, yet they solve different problems. Mixing them up is the single most common interview stumble on this topic.
Teacher handles every kind of teacher you ever add.if-else chains that test types simply disappear.EnglishTeacher class touches no existing file.Java splits polymorphism by when the decision happens. One kind settles at compile time. The other waits until the program runs.
This kind has two other names. Some books call it static binding. Others call it early binding. Method overloading is how we get it.
The compiler reads your call, looks at the argument types, and locks in one specific method. That choice never changes afterwards. By the time your program starts, the decision has already been made.
This kind also has other names. You will see it called dynamic binding, or late binding. Method overriding is how we get it.
Here the compiler only checks that the method exists on the reference type. The real choice waits. When the line finally executes, the JVM inspects the actual object and calls that object’s version.
Binding simply means linking a method call to a method body. Java binds some calls early and some late.
static, private, or finalOverloading gives one method name several parameter lists inside the same class. It exists to keep names simple. Nobody wants to recall addTwoInts(), addTwoDoubles(), and addThreeInts().
package com.javahandson;
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
public class OverloadDemo {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(5, 10)); // Output: 15
System.out.println(calc.add(5.0, 10.0)); // Output: 15.0
System.out.println(calc.add(5, 10, 15)); // Output: 30
}
}Three methods share the name add. The compiler tells them apart by counting and typing the arguments at each call site.
The parameter list must differ. Java accepts three kinds of difference:
add(int, int) against add(int, int, int)add(int, int) against add(double, double)add(int, double) against add(double, int)Beyond that, the rules are loose:
privatestatic, final, and private methods all overload without complaintadd(int...) counts as its own overloadChanging only the return type fails. The compiler rejects it flatly:
class Calculator {
int add(int a, int b) {
return a + b;
}
// double add(int a, int b) { // does not compile
// return a + b;
// }
// error: method add(int,int) is already defined in class Calculator
}Why so strict? Picture the call calc.add(5, 10); with the result thrown away. Java would have no way to tell which version you meant. Renaming the parameters does not help either, since parameter names never form part of a method signature.
Overload resolution runs in three passes, and it stops at the first pass that finds a match.
int may become long or doubleint may become Integer or ObjectThat order produces a result that surprises almost everyone:
package com.javahandson;
public class ResolutionDemo {
static void show(long value) { System.out.println("long version"); }
static void show(Integer value) { System.out.println("Integer version"); }
static void show(int... values) { System.out.println("varargs version"); }
public static void main(String[] args) {
show(5); // Output: long version
}
}An int literal matches Integer perfectly after boxing. Java still picks long, because widening wins in pass one and boxing never gets a turn. Delete the long version and the Integer version takes over.
Passing null has its own quirk. Java picks the most specific reference type available, so show(String) beats show(Object). Offer two unrelated types such as String and StringBuilder, and the compiler gives up with an “ambiguous” error.
Overriding replaces a parent method with a subclass version. This is where polymorphism earns its reputation.
package com.javahandson;
class Teacher {
void teach() {
System.out.println("Teacher teaches a subject");
}
}
class MathsTeacher extends Teacher {
@Override
void teach() {
System.out.println("MathsTeacher teaches algebra");
}
}
class ScienceTeacher extends Teacher {
@Override
void teach() {
System.out.println("ScienceTeacher teaches physics");
}
}
public class OverrideDemo {
public static void main(String[] args) {
Teacher[] staff = { new Teacher(), new MathsTeacher(), new ScienceTeacher() };
for (Teacher t : staff) {
t.teach();
}
}
}
// Output: Teacher teaches a subject
// Output: MathsTeacher teaches algebra
// Output: ScienceTeacher teaches physicsStudy that loop. It knows nothing about algebra or physics. Every element has the declared type Teacher, yet each one behaves according to the object it really holds.
None of this works without upcasting. Storing a subclass object in a superclass reference is what makes the choice interesting.
Teacher maths = new MathsTeacher(); // upcasting, no cast operator needed maths.teach(); // Output: MathsTeacher teaches algebra // maths.checkHomework(); // compile error if only MathsTeacher declares it
Notice the two halves here. The reference type sets which methods you may call. The object type sets which body then runs. Our guide to type casting in Java digs deeper into that distinction.
Dynamic method dispatch is the mechanism that resolves an overridden call at runtime. The name sounds intimidating, so let us walk through what the JVM does.
teach() exists on the reference type TeacherMathsTeacher, so it runs the MathsTeacher bodyUnder the hood, each class carries a method table, and the JVM jumps through that table. You get all of this for free by writing extends and an override.
You have used polymorphism many times already. Nobody gave it a name, that is all.
Think about the last time you typed this line:
List<String> names = new ArrayList<>();
names.add("Asha");
System.out.println(names.get(0)); // Output: AshaThat is an upcast. The List type is the contract. The ArrayList object does the work. Swap in a LinkedList tomorrow and the rest of your code will not notice.
The same trick hides in code you touch every day:
toString(), not the one in ObjectComparator sorts one list many ways, with no change to the listConnection typeBufferedReader and the source stops matteringNone of that needs new syntax. It is the same idea you just read about, at a larger scale.
Overriding comes with real limits. Break one and the compiler stops you. That is a good thing.
The method name and parameter list must match the parent exactly. Change a single parameter type and you have written an overload instead, quietly and by accident.
Return types have one soft rule. A child may return a subtype of what the parent returns. Java calls this a covariant return type.
package com.javahandson;
class Parent {
Number compute() {
return 10;
}
}
class Child extends Parent {
@Override
Integer compute() { // covariant return: Integer extends Number
return 42;
}
}
public class CovariantDemo {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.compute()); // Output: 42
}
}An override can open up access. It can never shut it down.
Picture the ladder: private, then package-private, then protected, then public. Your override may climb the ladder or stay put. Climbing down breaks every caller who reached the method through the parent type.
class Parent {
protected int twice(int n) { return 2 * n; }
}
class Child extends Parent {
@Override
public int twice(int n) { return 4 * n; } // widening: protected to public, fine
}
class Broken extends Parent {
// @Override
// int twice(int n) { return n; } // narrowing: protected to default
// error: attempting to assign weaker access privileges; was protected
}Unchecked exceptions have no rules here. Throw any RuntimeException you like from an override, whatever the parent says.
Checked exceptions are stricter. Your override may:
FileNotFoundException in place of IOExceptionException in place of IOException failspackage com.javahandson;
import java.io.FileNotFoundException;
import java.io.IOException;
class Reader {
void load() throws IOException { }
}
class FileReaderImpl extends Reader {
@Override
void load() throws FileNotFoundException { } // narrower, allowed
}
class QuietReader extends Reader {
@Override
void load() { } // none at all, allowed
}
class BadReader extends Reader {
// @Override
// void load() throws Exception { } // broader, rejected
// error: overridden method does not throw java.lang.Exception
}The logic is easy once you see it from the caller’s seat. Somebody holding a Reader reference wrote a catch block for IOException. A surprise Exception would sail straight past it.
Add @Override to every override you write. It costs one line and catches a whole category of bug.
class Parent {
void show() {
System.out.println("Parent show()");
}
}
class Child extends Parent {
@Override
void show(int number) { // typo: an extra parameter
System.out.println("Child show()");
}
// error: method does not override or implement a method from a supertype
}Drop the annotation and this compiles happily. You would have written a brand new overload, watched Parent.show() run instead of yours, and spent an afternoon hunting the reason.
Four things sit outside dynamic dispatch. Each one trips up beginners in a slightly different way.
A static method belongs to the class, never to an object. Declare one with the same signature in a subclass and you hide the parent version rather than override it.
package com.javahandson;
class Parent {
static void display() {
System.out.println("Parent static method");
}
}
class Child extends Parent {
static void display() {
System.out.println("Child static method");
}
}
public class HidingDemo {
public static void main(String[] args) {
Parent p1 = new Parent();
Parent p2 = new Child(); // the object really is a Child
Child c1 = new Child();
p1.display(); // Output: Parent static method
p2.display(); // Output: Parent static method
c1.display(); // Output: Child static method
}
}Look hard at the middle call. The object is a Child, yet the parent version runs, because the reference type decides. That result is the whole difference between hiding and overriding in one line.
A final method slams the door. No subclass may replace it, and the compiler says so with “overridden method is final”. Authors use final to protect logic that subclasses must not bend.
A private method is invisible outside its own class. A subclass never inherits it, so a same-named method in the child is simply a separate method that happens to share a name.
package com.javahandson;
class Parent {
private void secret() {
System.out.println("Parent secret");
}
void callSecret() {
secret(); // always the Parent version
}
}
class Child extends Parent {
private void secret() {
System.out.println("Child secret");
}
}
public class PrivateDemo {
public static void main(String[] args) {
new Child().callSecret(); // Output: Parent secret
}
}Constructors never get inherited, so nothing exists to override. A subclass writes its own, and super() chains up to the parent as the first statement.
Two constructors in one class with different parameters do count as overloading, though. That pattern turns up constantly.
Here is the trap that catches even experienced developers. Methods dispatch on the object. Fields resolve on the reference type.
package com.javahandson;
class Parent {
String label = "Parent field";
String describe() { return "Parent method"; }
}
class Child extends Parent {
String label = "Child field"; // hides, does not override
@Override
String describe() { return "Child method"; }
}
public class FieldDemo {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.label); // Output: Parent field
System.out.println(p.describe()); // Output: Child method
}
}Same object, same reference, two opposite answers. Avoid the confusion entirely: keep fields private and expose them through getters, which do dispatch dynamically.
Interviewers love this comparison, so here it is in one table.
| Aspect | Method Overloading | Method Overriding |
|---|---|---|
| Polymorphism type | Compile-time, static | Runtime, dynamic |
| Binding | Early binding | Late binding |
| Where it lives | Same class, or a subclass | Subclass only |
| Parameter list | Must differ | Must match exactly |
| Return type | Free, once parameters differ | Same type or a covariant one |
| Access modifier | Any | Same or wider |
| Checked exceptions | Unrestricted | Same, narrower, or none |
| Works on static methods | Yes | No, it hides instead |
| Inheritance needed | No | Yes |
| Decided by | Reference and argument types | Actual object type |
Overriding a concrete parent method works. Overriding a contract works better, because the parent then makes no promises about behaviour at all.
An interface declares what a type must do and says nothing about how. Every implementing class fills in the blanks its own way.
package com.javahandson;
interface Teacher {
void teach();
default void greet() { // default method, Java 8 onward
System.out.println("Good morning, class");
}
}
class MathsTeacher implements Teacher {
@Override
public void teach() {
System.out.println("MathsTeacher teaches algebra");
}
}
class ScienceTeacher implements Teacher {
@Override
public void teach() {
System.out.println("ScienceTeacher teaches physics");
}
}
public class InterfaceDemo {
public static void main(String[] args) {
// Teacher t = new Teacher(); // error: Teacher is abstract
Teacher maths = new MathsTeacher();
maths.greet(); // Output: Good morning, class
maths.teach(); // Output: MathsTeacher teaches algebra
Teacher science = new ScienceTeacher();
science.teach(); // Output: ScienceTeacher teaches physics
}
}Java forbids new Teacher() because an interface has no body to construct. That restriction is the point. It pushes you to depend on the contract instead of a concrete class.
An abstract class sits halfway. It can hold shared state and finished methods, plus abstract methods that subclasses must implement.
package com.javahandson;
abstract class Staff {
String name;
Staff(String name) {
this.name = name;
}
void checkIn() { // shared, already written
System.out.println(name + " checked in");
}
abstract void work(); // each subclass decides
}
class Librarian extends Staff {
Librarian(String name) { super(name); }
@Override
void work() {
System.out.println(name + " shelves books");
}
}
public class AbstractDemo {
public static void main(String[] args) {
Staff s = new Librarian("Asha");
s.checkIn(); // Output: Asha checked in
s.work(); // Output: Asha shelves books
}
}Comparable or Runnablesealed interface, added in Java 17, when you want to control exactly who implements itRead our companion guides on the abstract class in Java and on abstraction in Java for the full comparison.
Most polymorphism bugs come from a short list. Watch for these:
@Override catches it at once.p.label follows the declared type.null or 0.instanceof checks mean a method is missing from the parent type.public back to protected refuses to compile.That fourth item deserves a demonstration, because it looks impossible until you see it:
package com.javahandson;
class Base {
Base() {
print(); // calls the overridden version
}
void print() {
System.out.println("Base print");
}
}
class Derived extends Base {
String message = "Derived ready";
@Override
void print() {
System.out.println(message);
}
}
public class ConstructorTrap {
public static void main(String[] args) {
new Derived(); // Output: null
}
}The parent constructor runs first. It jumps to Derived.print(). But message has no value yet, so null prints. So keep constructors away from methods a child can replace.
Let us close with one small program that uses every idea above. It sends notifications through different channels, overloads a helper, overrides a contract, and leans on dynamic dispatch.
package com.javahandson;
import java.util.ArrayList;
import java.util.List;
abstract class Notification {
String recipient;
Notification(String recipient) {
this.recipient = recipient;
}
abstract void send(String message); // subclasses override this
void send(String message, int times) { // overload, same class
for (int i = 0; i < times; i++) {
send(message); // dynamic dispatch
}
}
}
class EmailNotification extends Notification {
EmailNotification(String recipient) { super(recipient); }
@Override
void send(String message) {
System.out.println("Email to " + recipient + ": " + message);
}
}
class SmsNotification extends Notification {
SmsNotification(String recipient) { super(recipient); }
@Override
void send(String message) {
System.out.println("SMS to " + recipient + ": " + message);
}
}
public class NotifyDemo {
public static void main(String[] args) {
List<Notification> outbox = new ArrayList<>();
outbox.add(new EmailNotification("suraj@example.com")); // upcast
outbox.add(new SmsNotification("9876543210")); // upcast
for (Notification n : outbox) {
n.send("Your order has shipped");
}
outbox.get(1).send("Please rate us", 2); // overloaded version
}
}
// Output: Email to suraj@example.com: Your order has shipped
// Output: SMS to 9876543210: Your order has shipped
// Output: SMS to 9876543210: Please rate us
// Output: SMS to 9876543210: Please rate usThree ideas share one file here. The two-argument send() is compile-time polymorphism, chosen by argument count. The one-argument send() is runtime polymorphism, chosen by the object. And the list upcasts both subclasses to Notification, which is what lets the loop stay so short.
Now add a PushNotification class. You write one new file, add one line to the outbox, and change nothing else. That is the payoff polymorphism promises.
A: Polymorphism in Java lets one method name take many forms. The same call behaves differently depending on the arguments you pass or the object behind the reference.
A: Compile-time polymorphism comes from method overloading, where the compiler picks the method. Runtime polymorphism comes from method overriding, where the JVM picks it while the program runs.
A: Overloading needs different parameter lists in the same class and resolves at compile time. Overriding needs an identical signature in a subclass and resolves at runtime against the actual object.
A: No. A same-signature static method in a subclass hides the parent version instead. The reference type then decides which one runs, so no dynamic dispatch happens.
A: Dynamic method dispatch resolves an overridden call at runtime. A superclass reference points at a subclass object, and the JVM runs the subclass version of the method.
A: No. Two methods with identical names and parameter lists clash, whatever their return types. A call that discards the result would leave the compiler no way to choose.
A: A covariant return type lets an override return a subclass of the parent’s return type. Returning Integer where the parent returns Number works fine, and Java has allowed this since version 5.
A: No. An override may declare the same checked exception, a subclass of it, or none. Widening to Exception would break callers who only catch the parent’s declared type.
A: No. Fields resolve against the reference type at compile time, so a subclass field hides the parent field rather than overriding it. Keep fields private and use getters to get polymorphic behaviour.
A: No. Java deliberately leaves it out to keep code readable. The one exception is + for string concatenation, and the language itself builds that in rather than letting you define it.
Let us wrap up what we covered. Polymorphism in Java means one name, many forms, and Java delivers it in two separate ways.
Method overloading is the compile-time half. Several methods share a name inside one class, and the compiler picks one from the argument types. Widening beats boxing, and boxing beats varargs.
Method overriding is the runtime half. A subclass supplies its own body, and the JVM picks it based on the real object. Upcasting sets that up, and dynamic method dispatch carries it out.
Then there are the boundaries. Static methods hide instead of overriding. Fields hide too. Constructors, final methods, and private methods stay off limits entirely.
Interfaces and abstract classes push all of this further. Program against a contract, and every new implementation drops in without touching a line of the code that calls it.
Here is the habit worth building. Whenever you catch yourself writing if (obj instanceof Something), stop and ask whether a method on the parent type would say it better. Nine times out of ten, it would.