Java Polymorphism
Updated 10 min read Aimerscrew
Java polymorphism lets you write code that works with a general type, like Shape or Animal, while each real object behaves in its own special way. The word comes from Greek and means "many forms". In this lesson, you'll see how one variable can hold many kinds of objects, how Java decides which method to run, and how to safely convert between parent and child types with casting and instanceof.
🌍 Real-life example
Think of the "Pay" button in an online store. You press the same button whether you chose a credit card, PayPal or a gift card. The store's code just says "pay", and each payment method knows how to do its own job. One action, many forms: that's polymorphism.
What is polymorphism in Java?
Polymorphism is the ability of a single reference type to refer to objects of different classes, with each object answering the same method call in its own way. It builds directly on inheritance and method overriding.
The key rule: a superclass variable can hold a subclass object. A Circle is a Shape, so a Shape variable may point to a Circle.
public class ShapeDemo {
public static void main(String[] args) {
Shape s = new Circle(2);
System.out.printf("%s area: %.2f%n", s.name(), s.area());
s = new Square(3);
System.out.printf("%s area: %.2f%n", s.name(), s.area());
}
}
class Shape {
String name() {
return "Shape";
}
double area() {
return 0;
}
}
class Circle extends Shape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
String name() {
return "Circle";
}
@Override
double area() {
return Math.PI * radius * radius;
}
}
class Square extends Shape {
private double side;
Square(double side) {
this.side = side;
}
@Override
String name() {
return "Square";
}
@Override
double area() {
return side * side;
}
}
Circle area: 12.57
Square area: 9.00
The line s.area() appears twice and looks identical, yet it runs different code each time. The variable's type is always Shape; what changes is the object it points to.
Reference type vs object type
Every polymorphic line has two types, and it helps to name them:
- The reference type (or declared type) is the type of the variable:
ShapeinShape s. It decides which methods you are allowed to call. The compiler checks this. - The object type (or runtime type) is the class used with
new:Circle. It decides which version of the method actually runs.
So s.area() compiles because Shape has an area() method, and it runs Circle's area() because the object is a Circle.
How does dynamic method dispatch work?
Dynamic method dispatch is how Java chooses an overridden method while the program runs. When you call s.area(), the JVM follows the reference to the real object, checks its class, and runs that class's version of area(). If the class didn't override it, the JVM walks up to the parent class and uses that version.
s.area(), goes to whichever class the object really belongs to.This is why it's also called run-time polymorphism: the decision can't be made earlier, because a variable might point to a different object each time the line runs.
Arrays of a supertype
Polymorphism really shines when you store different objects together. An array of Notification can hold emails, text messages and push alerts, and one loop handles them all:
public class NotificationCenter {
public static void main(String[] args) {
Notification[] outbox = {
new EmailNotification("maya@example.com"),
new SmsNotification("+44 7700 900123"),
new PushNotification("Maya's phone")
};
for (Notification n : outbox) {
n.send("Your order has shipped!");
}
}
}
class Notification {
void send(String message) {
System.out.println("Sending: " + message);
}
}
class EmailNotification extends Notification {
private String address;
EmailNotification(String address) {
this.address = address;
}
@Override
void send(String message) {
System.out.println("Email to " + address + ": " + message);
}
}
class SmsNotification extends Notification {
private String phone;
SmsNotification(String phone) {
this.phone = phone;
}
@Override
void send(String message) {
System.out.println("SMS to " + phone + ": " + message);
}
}
class PushNotification extends Notification {
private String device;
PushNotification(String device) {
this.device = device;
}
@Override
void send(String message) {
System.out.println("Push to " + device + ": " + message);
}
}
Email to maya@example.com: Your order has shipped!
SMS to +44 7700 900123: Your order has shipped!
Push to Maya's phone: Your order has shipped!
The loop has no if statements checking "is this an email or an SMS?". If next month you add a WhatsAppNotification, the loop doesn't change at all. You just add a new subclass. The same works with lists, such as an ArrayList<Notification>, which you'll meet in Java ArrayList.
💡 Tip
When you see a long if/else chain that checks what kind of object something is, ask yourself: could each class override a method instead? Polymorphism often replaces that chain with one line.
Upcasting and downcasting
Casting objects means viewing an object through a different reference type. There are two directions:
- Upcasting: subclass to superclass (
DogtoAnimal). It is always safe, so Java does it automatically. - Downcasting: superclass to subclass (
AnimaltoDog). It might fail, so you must write the cast yourself in parentheses, like you did for numbers in Java type casting.
public class PetShelter {
public static void main(String[] args) {
Animal pet = new Dog("Biscuit"); // upcasting: automatic
pet.speak(); // Dog's version runs
// pet.fetch(); // compile error: Animal has no fetch()
Dog dog = (Dog) pet; // downcasting: explicit
dog.fetch();
}
}
class Animal {
protected String name;
Animal(String name) {
this.name = name;
}
void speak() {
System.out.println(name + " makes a sound");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void speak() {
System.out.println(name + " says Woof!");
}
void fetch() {
System.out.println(name + " runs after the ball");
}
}
Biscuit says Woof!
Biscuit runs after the ball
Even though pet points to a real Dog, you can't call pet.fetch(): the compiler only knows the reference type, Animal, and not every animal can fetch. The downcast (Dog) pet tells the compiler "trust me, it's a Dog".
What is a ClassCastException?
If you downcast to the wrong type, the program compiles but crashes when it runs. For example, if pet pointed to a Cat, the line Dog dog = (Dog) pet; would stop the program with an error that starts like this:
Exception in thread "main" java.lang.ClassCastException: class Cat cannot be cast to class Dog
A cat can't suddenly become a dog. To avoid this crash, check the type before you cast, using instanceof. (You'll learn to handle errors like this in Java exceptions.)
instanceof with pattern matching
The instanceof operator asks "is this object an instance of that class (or one of its subclasses)?" and gives true or false. Since Java 16, it can also cast and name the variable in one step. This is called pattern matching for instanceof:
public class ShelterTour {
public static void main(String[] args) {
Animal[] animals = { new Dog("Biscuit"), new Cat("Luna"), new Dog("Rex") };
for (Animal a : animals) {
a.speak();
if (a instanceof Dog d) { // test and cast in one step
d.fetch();
}
}
}
}
class Animal {
protected String name;
Animal(String name) {
this.name = name;
}
void speak() {
System.out.println(name + " makes a sound");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
void speak() {
System.out.println(name + " says Woof!");
}
void fetch() {
System.out.println(" " + name + " runs after the ball");
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
void speak() {
System.out.println(name + " says Meow!");
}
}
Biscuit says Woof!
Biscuit runs after the ball
Luna says Meow!
Rex says Woof!
Rex runs after the ball
Read a instanceof Dog d as: "if a is a Dog, give me a Dog variable called d that points to it". Inside the if block, d is already the right type, so no separate cast is needed. The older style needed two steps:
if (a instanceof Dog) {
Dog d = (Dog) a; // old style: test, then cast
d.fetch();
}
⚠️ Be careful
Use instanceof for special cases only, like calling a method that just one subclass has. If you find yourself checking every type, move the behavior into an overridden method instead.
Overloading vs overriding: compile-time vs run-time
Both features let one method name do different things, which confuses many beginners. Method overloading is sometimes called compile-time polymorphism, and overriding is run-time polymorphism. This program uses both:
public class CheckoutCounter {
public static void main(String[] args) {
Receipt plain = new Receipt();
Receipt gift = new GiftReceipt();
// Overloading: chosen by the compiler from the argument types
plain.addLine("Coffee beans");
plain.addLine("Mug", 2);
// Overriding: chosen at run time from the real object
plain.footer();
gift.footer();
}
}
class Receipt {
void addLine(String item) {
System.out.println("1 x " + item);
}
void addLine(String item, int quantity) {
System.out.println(quantity + " x " + item);
}
void footer() {
System.out.println("Thanks for shopping!");
}
}
class GiftReceipt extends Receipt {
@Override
void footer() {
System.out.println("Prices hidden. Enjoy your gift!");
}
}
1 x Coffee beans
2 x Mug
Thanks for shopping!
Prices hidden. Enjoy your gift!
| Overloading | Overriding | |
|---|---|---|
| What it is | Same name, different parameters | Same name and same parameters in a subclass |
| Where | Usually in the same class | Superclass and subclass |
| Decided when? | Compile time, from argument types | Run time, from the object's real class |
| Also called | Static or compile-time polymorphism | Dynamic or run-time polymorphism |
| Needs inheritance? | No | Yes |
| Annotation | None | @Override |
Common mistakes with polymorphism
1. Calling a subclass-only method through a superclass reference
Animal pet = new Dog("Rex"); pet.fetch(); fails with cannot find symbol. The compiler only looks at the reference type. Use instanceof with a pattern variable, or declare the variable as Dog if you always need Dog features.
2. Downcasting without checking
A cast like (Dog) pet compiles even when pet is a Cat, and crashes at run time with a ClassCastException. Guard risky casts with if (pet instanceof Dog d).
3. Expecting fields to be polymorphic
Only methods are dispatched at run time. If a parent and child both declare a field called name, the reference type decides which one you read. Don't redeclare parent fields in a subclass; use methods instead.
4. Thinking static methods can be overridden
Static methods belong to the class, not to an object, so they are not dispatched dynamically. A static method with the same signature in a subclass just hides the parent one.
Practice exercise
🏋️ Try it yourself
Create an Instrument class with a play() method, and three subclasses: Guitar, Drum and Piano, each overriding play(). Give Guitar an extra method tune(). Put four instruments in an Instrument[] array, play each one, call tune() only on guitars, and print how many guitars are on stage.
Show solution
public class Band {
public static void main(String[] args) {
Instrument[] stage = { new Guitar(), new Drum(), new Guitar(), new Piano() };
int guitars = 0;
for (Instrument i : stage) {
i.play();
if (i instanceof Guitar g) {
g.tune();
guitars++;
}
}
System.out.println("Guitars on stage: " + guitars);
}
}
class Instrument {
void play() {
System.out.println("...");
}
}
class Guitar extends Instrument {
@Override
void play() {
System.out.println("Guitar: strum strum");
}
void tune() {
System.out.println(" (tuning the guitar)");
}
}
class Drum extends Instrument {
@Override
void play() {
System.out.println("Drum: boom tak");
}
}
class Piano extends Instrument {
@Override
void play() {
System.out.println("Piano: plink plonk");
}
}
Guitar: strum strum
(tuning the guitar)
Drum: boom tak
Guitar: strum strum
(tuning the guitar)
Piano: plink plonk
Guitars on stage: 2
What's next?
Did you notice something odd? Our base classes had placeholder methods, like Shape.area() returning 0 or Instrument.play() printing "...", that nobody really wants to call. Java has a cleaner solution for that. Next, learn about Java abstract classes. For comparison, see how Python handles the same idea in Python polymorphism.
Key takeaways
- Polymorphism lets a superclass reference, like Shape s, point to any subclass object, like a Circle or a Square.
- The reference type decides which methods you can call; the object type decides which version actually runs.
- Dynamic method dispatch picks the overridden method at run time by looking at the real object.
- Upcasting (child to parent) is automatic and safe; downcasting (parent to child) needs an explicit cast and can throw ClassCastException.
- Use pattern matching, if (a instanceof Dog d), to test and cast safely in one step.
- Overloading is decided at compile time from parameters; overriding is decided at run time from the object.
Frequently asked questions
What is polymorphism in Java in simple words?
Polymorphism means "many forms". In Java, one variable of a parent type can hold objects of different child classes, and calling the same method on it runs each child class's own version.
What are the two types of polymorphism in Java?
Compile-time polymorphism, which is method overloading (the compiler picks the method from the argument types), and run-time polymorphism, which is method overriding (the JVM picks the method from the real object while the program runs).
What is dynamic method dispatch?
It is the process Java uses to decide which overridden method to call at run time. The JVM looks at the actual class of the object, not the type of the variable, and runs that class's version of the method.
What is the difference between upcasting and downcasting in Java?
Upcasting converts a subclass reference to a superclass type, such as Animal a = new Dog(), and happens automatically. Downcasting converts back, such as Dog d = (Dog) a, and needs an explicit cast that fails with ClassCastException if the object is not really that type.
What does instanceof do in Java?
instanceof checks whether an object is an instance of a class or one of its subclasses and returns true or false. With pattern matching, as in if (a instanceof Dog d), it also creates a variable of the right type that you can use right away.