Polymorphism
What Is Polymorphism?
Polymorphism means "many forms." In OOP, it describes a single method call or a single interface that behaves differently depending on what it's actually operating on. You call the exact same method, through the exact same reference type, but the behavior that runs depends on the real, runtime type of the object underneath.
Java gives you polymorphism in two distinct flavors: one resolved while the code is being compiled, and one resolved while the program is actually running.
Kind | Mechanism | Resolved |
Compile-time (static) polymorphism | Method overloading — multiple methods with the same name but different parameter lists | At compile time, based on the arguments' declared types |
Runtime (dynamic) polymorphism | Method overriding — a subclass replaces an inherited method's implementation | At runtime, based on the object's actual type |
Compile-Time Polymorphism: Overloading
Method overloading lets a class define several methods with the same name but different parameter lists. The compiler decides, at compile time, exactly which overload a given call matches, purely by looking at the number and types of the arguments.
Overloading — resolved at compile time
public class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}Runtime Polymorphism: Overriding
Runtime polymorphism comes from method overriding: a subclass provides its own implementation of a method that's already defined in its superclass (or an interface it implements). When you call that method through a superclass-typed reference, Java looks at the object's actual runtime type to decide which version to run — not the type of the variable holding the reference. This lookup is called dynamic dispatch, and it is the mechanism the rest of this page demonstrates.
Worked Example: Dynamic Dispatch
Here, three different subclasses override the same makeSound() method. A single array of the superclass type Animal holds objects of every subclass, and calling makeSound() on each element produces different output — even though every call in the loop is written identically.
One method call, three different behaviors
public class Animal {
void makeSound() {
System.out.println("The animal makes a sound");
}
}
public class Dog extends Animal {
@Override
void makeSound() {
System.out.println("The dog barks");
}
}
public class Cat extends Animal {
@Override
void makeSound() {
System.out.println("The cat meows");
}
}
public class Cow extends Animal {
@Override
void makeSound() {
System.out.println("The cow moos");
}
}
public class Main {
public static void main(String[] args) {
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal a : animals) {
a.makeSound(); // same call, different behavior each time
}
}
}The dog barks The cat meows The cow moos
Every element of the animals array is declared as type Animal, and every loop iteration calls the exact same expression, a.makeSound(). Yet the output differs each time, because Java resolves which makeSound() to actually run based on each object's real, underlying type (Dog, Cat, or Cow) rather than the declared reference type (Animal). That's runtime polymorphism in action.
Why Polymorphism Matters
You can write code against a general superclass or interface type and have it automatically work correctly for any subclass, present or future
Adding a new subclass (say, a Bird that overrides makeSound()) requires no changes to the loop in Main — it just works
This is the foundation for extensible designs: frameworks, plugin systems, and collections of mixed but related objects all lean on runtime polymorphism