What Is Polymorphism in Java?

Polymorphism means "many forms" — in Java it means that a single variable, parameter or return type can refer to objects of different concrete classes and the right code runs automatically based on the actual type at runtime.

Runtime polymorphism (method overriding)

public class Animal {
    public String speak() { return "..."; }
}

public class Dog extends Animal {
    @Override public String speak() { return "Woof!"; }
}

public class Cat extends Animal {
    @Override public String speak() { return "Meow!"; }
}

Animal a = new Dog(); // Dog stored in an Animal reference
System.out.println(a.speak()); // Woof! — Dog's version runs

a = new Cat();
System.out.println(a.speak()); // Meow! — Cat's version runs

The variable a is typed Animal, but Java dispatches speak() to the actual object's class at runtime. This is called dynamic dispatch and is the core of OO polymorphism.

Interface-based polymorphism

List<String> list = new ArrayList<>();  // or LinkedList, CopyOnWriteArrayList...
list.add("hello");

// Same call works regardless of which List implementation is behind the variable:
void process(List<String> items) {
    for (String item : items) System.out.println(item);
}

Code that programs to an interface (here List) is decoupled from the implementation. Swap ArrayList for LinkedListprocess doesn't change.

Compile-time polymorphism (method overloading)

public void print(int n)    { System.out.println("int: " + n); }
public void print(double d) { System.out.println("double: " + d); }
public void print(String s) { System.out.println("string: " + s); }

print(42);      // int: 42
print(3.14);    // double: 3.14
print("Java");  // string: Java

The compiler picks the correct overload at compile time based on the argument types. This is not "true" polymorphism (no runtime dispatch) but is often grouped under the same umbrella.

The Liskov Substitution Principle

Polymorphism works correctly when subclasses honour the contract of their parent: anywhere a Shape is expected, a Circle should work correctly without the caller knowing or caring it is a circle. This rule (from Barbara Liskov) is the "L" in SOLID principles.

When polymorphism helps

  • Plugin systems: inject any PaymentProvider implementation (Stripe, PayPal, Braintree) without changing calling code.
  • Testing: inject a mock/stub implementation that satisfies the interface without hitting a real database or HTTP service.
  • Collections: List<Shape> holds circles and rectangles; a single loop calls area() on all of them via polymorphism.