Polymorphism in Java

By the Syncfusion DevTutorials team · Last updated

Use one name for many behaviours with overloading and overriding, @Override, dynamic method dispatch, and safe upcasting and downcasting.

Think about the word "open".

You open a door. You open a book. You open an app on your phone. The word is the same each time, but what actually happens is different. It depends on what you are opening.

Java works the same way. One method name can do different things, depending on the values you pass or the object you call it on.

In simple words: polymorphism means one name, many forms.

The word comes from Greek: poly means "many", and morph means "form".

What You Will Learn

In this lesson, you will learn:

  1. What polymorphism is, and why it makes code flexible.
  2. How method overloading gives compile-time polymorphism.
  3. How method overriding gives runtime polymorphism.
  4. Why you should always use the @Override annotation.
  5. How Java picks the right method while the program runs.
  6. How upcasting and downcasting work, and how to downcast safely.
  7. The differences between compile-time and runtime polymorphism.

Start With a Working Example

Press Run.

One call, three different results

public class Main {
    public static void main(String[] args) {
        Animal[] zoo = { new Dog(), new Cat(), new Cow() };

        for (Animal a : zoo) {
            a.speak();
        }
    }
}

class Animal {
    void speak() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    void speak() {
        System.out.println("Dog says Woof");
    }
}

class Cat extends Animal {
    @Override
    void speak() {
        System.out.println("Cat says Meow");
    }
}

class Cow extends Animal {
    @Override
    void speak() {
        System.out.println("Cow says Moo");
    }
}

Expected output:

Dog says Woof
Cat says Meow
Cow says Moo

The loop always calls a.speak(), and a is always an Animal variable. Yet each animal makes its own sound. Java runs the version that belongs to the real object.

Try This

Add a Duck class that extends Animal and overrides speak(). Put a new Duck() in the array. The loop works without any change.

Introduction to Polymorphism

Polymorphism means one name can have many forms.

In Java, it shows up in two ways:

KindAlso calledHow it worksDecided
Compile-time polymorphismStatic polymorphismMethod overloadingWhen the code is compiled
Runtime polymorphismDynamic polymorphismMethod overridingWhile the program runs

Why it matters:

  • Flexible code. One loop can handle many types of object.
  • Easy to extend. A new class works with old code, as long as it keeps the same method names.
  • Less if-else. You do not need checks like "if it is a dog, bark; if it is a cat, meow". Each object knows its own behaviour.

Compile-Time Polymorphism

Compile-time polymorphism happens when the compiler decides which method to call, before the program runs.

It looks only at the method name and the arguments you pass.

Method Overloading

Method overloading means several methods in the same class have the same name but different parameters.

You learned this in the Methods lesson. Here it is again, as a form of polymorphism.

The compiler picks a version from the arguments

public class Main {
    public static void main(String[] args) {
        Printer p = new Printer();

        p.print(42);
        p.print("Hello");
        p.print(3.5);
        p.print("Total", 250);
    }
}

class Printer {
    void print(int number) {
        System.out.println("Number: " + number);
    }

    void print(String text) {
        System.out.println("Text: " + text);
    }

    void print(double value) {
        System.out.println("Decimal: " + value);
    }

    void print(String label, int value) {
        System.out.println(label + " = " + value);
    }
}

Expected output:

Number: 42
Text: Hello
Decimal: 3.5
Total = 250

All four methods are called print(). The compiler looks at each call's arguments and chooses the matching version.

The choice is made when the code is compiled. That is why this is called compile-time polymorphism.

Runtime Polymorphism

Runtime polymorphism happens when Java decides which method to call while the program runs, based on the actual object.

Method Overriding

Method overriding means a child class writes its own version of a method it inherited from its parent.

The overriding method must have:

  • the same name as the parent's method;
  • the same parameters, in the same order;
  • the same return type, or a child type of it;
  • the same or wider access. A public method cannot become private in the child.

You cannot override static, final or private methods.

Each child gives the inherited method its own body

public class Main {
    public static void main(String[] args) {
        Payment[] payments = {
            new CardPayment(),
            new UpiPayment(),
            new CashPayment()
        };

        for (Payment p : payments) {
            p.pay(500);
        }
    }
}

class Payment {
    void pay(int amount) {
        System.out.println("Paying " + amount);
    }
}

class CardPayment extends Payment {
    @Override
    void pay(int amount) {
        System.out.println("Card: charged " + amount + " plus 2% fee = " + (amount + amount * 2 / 100));
    }
}

class UpiPayment extends Payment {
    @Override
    void pay(int amount) {
        System.out.println("UPI: sent " + amount + " with no fee");
    }
}

class CashPayment extends Payment {
    @Override
    void pay(int amount) {
        System.out.println("Cash: collect " + amount + " at delivery");
    }
}

Expected output:

Card: charged 500 plus 2% fee = 510
UPI: sent 500 with no fee
Cash: collect 500 at delivery

The checkout code only calls p.pay(500). It does not need to know which payment method the customer chose.

OverloadingOverriding
WhereIn the same classIn a parent and a child class
Method nameSameSame
ParametersMust be differentMust be the same
Needs inheritance?NoYes
DecidedAt compile timeAt run time
PurposeSame job, different inputsSame job, different behaviour per type

@Override Annotation

An annotation is a note for the compiler, written with @.

@Override goes on the line above a method that is meant to override a parent method. It tells the compiler: "check that this really overrides something".

Without it, a small typo creates a new method instead of overriding the old one. The program still compiles, but the parent's version keeps running, and the bug is hard to find.

@Override catches a misspelled method name

public class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.speak();
    }
}

class Animal {
    void speak() {
        System.out.println("Some sound");
    }
}

class Dog extends Animal {
    @Override
    void speek() {
        System.out.println("Woof");
    }
}

The compiler reports method does not override or implement a method from a supertype. The typo speek was caught straight away.

Without @Override, this program would compile and quietly print Some sound.

Easy Way to Remember: every time you override, write @Override. It costs one line and catches typos in names and parameters.

Dynamic Method Dispatch

Dynamic method dispatch is how Java carries out runtime polymorphism.

Two types are involved whenever you use a variable:

  • The reference type is the type of the variable: Animal a.
  • The object type is the type of the object it points to: new Dog().

When you call an overridden method, Java looks at the object type, not the reference type. It finds that object's own version and runs it.

The decision is made while the program runs, because the same variable might point to a different kind of object each time.

The object type decides, not the variable type

public class Main {
    public static void main(String[] args) {
        Shape s;

        s = new Circle();
        s.draw();

        s = new Square();
        s.draw();

        s = new Shape();
        s.draw();
    }
}

class Shape {
    void draw() {
        System.out.println("Drawing a shape");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle");
    }
}

class Square extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a square");
    }
}

Expected output:

Drawing a circle
Drawing a square
Drawing a shape

s is always a Shape variable. But it points to a Circle, then a Square, then a plain Shape. Each call runs the version that belongs to the object at that moment.

What Is Not Dispatched

Dynamic dispatch only applies to overridden instance methods.

Fields and static methods are chosen by the reference type, at compile time.

Fields follow the variable type, methods follow the object

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();

        System.out.println("Field: " + p.name);
        System.out.println("Method: " + p.getName());
    }
}

class Parent {
    String name = "Parent field";

    String getName() {
        return "Parent method";
    }
}

class Child extends Parent {
    String name = "Child field";

    @Override
    String getName() {
        return "Child method";
    }
}

Expected output:

Field: Parent field
Method: Child method

The field was chosen by the variable type, Parent. The method was chosen by the object type, Child.

This is one more reason to keep fields private and reach data through methods.

Upcasting and Downcasting

Casting an object means treating it as a different type in the same family.

Upcasting

Upcasting means storing a child object in a parent-type variable.

Animal a = new Dog();   // upcasting

It is always safe, because every Dog is an Animal. Java does it automatically, with no cast written.

After upcasting, you can only call methods the parent type knows about. The Dog object still has its own methods, but the Animal variable cannot see them.

Downcasting

Downcasting means turning a parent-type variable back into a child type, so you can use the child's extra methods.

Dog d = (Dog) a;   // downcasting

You must write the cast yourself. It is not always safe. If a actually points to a Cat, the cast fails while the program runs, with a ClassCastException.

Checking With instanceof

Before downcasting, check the real type with instanceof. It returns true if the object is that type (or a child of it).

Modern Java lets you check and cast in one step, with a pattern:

if (a instanceof Dog d) {
    d.fetch();   // d is already a Dog here
}

Upcast freely, downcast only after checking

public class Main {
    public static void main(String[] args) {
        Animal[] pets = { new Dog(), new Cat(), new Dog() };

        for (Animal a : pets) {
            a.eat();

            if (a instanceof Dog d) {
                d.fetch();
            } else if (a instanceof Cat c) {
                c.climb();
            }
        }
    }
}

class Animal {
    void eat() {
        System.out.println("Eating...");
    }
}

class Dog extends Animal {
    void fetch() {
        System.out.println("  Dog fetches the ball");
    }
}

class Cat extends Animal {
    void climb() {
        System.out.println("  Cat climbs the tree");
    }
}

Expected output:

Eating...
  Dog fetches the ball
Eating...
  Cat climbs the tree
Eating...
  Dog fetches the ball

Storing the pets in an Animal[] is upcasting. Every pet can eat().

fetch() only exists on Dog. So the loop checks a instanceof Dog d before using it. For the cat, that check is false, so no unsafe cast happens.

UpcastingDowncasting
DirectionChild → parentParent → child
ExampleAnimal a = new Dog();Dog d = (Dog) a;
Cast written?No, automaticYes
Always safe?YesNo, check with instanceof first
Methods you can callOnly the parent'sThe child's too

A wrong downcast compiles fine, then stops the program with ClassCastException: class Cat cannot be cast to class Dog. Always check with instanceof before you downcast.

static methods are not overridden. If a child declares a static method with the same signature, it hides the parent's one instead. The version called is chosen by the variable's type, not the object's.

Compile-Time vs Runtime Polymorphism

Compile-time polymorphismRuntime polymorphism
Achieved byMethod overloadingMethod overriding
Decided byThe compilerThe JVM, while the program runs
Based onThe arguments' types and countThe actual object's type
Needs inheritance?NoYes
Methods involvedSeveral methods in one classThe same method in parent and child
SpeedSlightly faster, no run-time lookupTiny run-time lookup
FlexibilityLowerHigher, new types work with old code
Exampleprint(int) and print(String)Dog.speak() overriding Animal.speak()

In simple words: overloading picks by what you pass. Overriding picks by what you call it on.

Common mistakes

The following mistakes are common when overriding methods and using polymorphism. Each row shows what you see, why it happens and how to fix it.

SymptomCauseFix
The parent's method runs instead of the child'sThe child changed the name or parametersMatch the signature, and add @Override
method does not override or implement a method@Override found no matching parent methodFix the name or parameters
weaker access privilegesThe overriding method is less visibleUse the same or wider access
cannot find symbol on a child methodThe variable's type is the parentCheck with instanceof, then use the child type
ClassCastExceptionDowncast to a type the object is notCheck with instanceof first
overridden method is finalThe parent method is finalDo not override it, or remove final

What to Remember

  • Polymorphism means one name with many forms.
  • Overloading gives compile-time polymorphism: same name, different parameters, chosen by the compiler.
  • Overriding gives runtime polymorphism: a child replaces a parent method, and the real object decides which version runs.
  • Always write @Override. It catches typos in names and parameters.
  • Dynamic method dispatch picks methods by the object's type. Fields and static methods follow the variable's type.
  • Upcasting is automatic and safe. Downcasting needs a cast, so check with instanceof first.

Next: Abstraction