Polymorphism

Polymorphism literally means "many forms." In Java, it describes a situation where the same method call behaves differently depending on the actual type of the object it's called on. It's one of the core pillars of object-oriented programming, alongside encapsulation, abstraction, and inheritance.

Two Kinds of Polymorphism

TypeHow it worksResolved
Runtime polymorphismMethod overriding — a subclass provides its own version of a method already defined in its superclassAt runtime, based on the actual object
Compile-time polymorphismMethod overloading — multiple methods with the same name but different parametersAt compile time, based on the arguments

Runtime Polymorphism (Method Overriding)

Recall the Animal, Dog, and Cat example from the inheritance lesson. Each subclass overrides a speak() method defined in Animal. If we store different animals in a single List<Animal> and call speak() on each one, Java picks the correct overridden version at runtime — based on what the object actually is, not the reference type used to hold it.

Example: Same Call, Different Behavior

class Animal {
  public void speak() {
    System.out.println("The animal makes a sound");
  }
}

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

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

public class PolymorphismDemo {
  public static void main(String[] args) {
    List<Animal> animals = new ArrayList<>();
    animals.add(new Dog());
    animals.add(new Cat());

    for (Animal a : animals) {
      a.speak(); // Dog prints "Woof!", Cat prints "Meow!"
    }
  }
}

What's happening:

  • Every element in the list is referenced as an Animal, but each one is actually a Dog or a Cat.
  • Calling a.speak() runs the version of speak() that belongs to the real object, not the Animal version.
  • This lets you write code that works with the general Animal type while still getting specific behavior for each subclass — no if/else chains checking types needed.

Upcasting

Treating a subclass object as its superclass type — like assigning a Dog to an Animal reference — is called upcasting. It happens automatically and safely, because a Dog is an Animal. Upcasting is exactly what makes the list example above possible: the list only needs to know about Animal, yet it can hold any subclass of it.

Animal a = new Dog(); // upcasting: Dog treated as an Animal
a.speak(); // still calls Dog's overridden speak()
          
Note: Even though a is declared as type Animal, only Animal's methods are visible on it directly. But when an overridden method is called, Java still runs the Dog version — this is the essence of runtime polymorphism.

Compile-Time "Polymorphism" (Method Overloading)

Method overloading is often called compile-time polymorphism because the compiler decides which method to call based purely on the number and types of arguments — before the program ever runs. It's a different mechanism from overriding, but it shares the same "one name, many behaviors" spirit.

class Calculator {
  public int add(int x, int y) { return x + y; }
  public double add(double x, double y) { return x + y; }
}
          

Both methods are named add, but the compiler picks the right one based on whether you pass ints or doubles. We covered this in more depth in the Methods lesson — it's mentioned here just to contrast it with runtime polymorphism.

Tip: If you're ever unsure whether something is overriding or overloading — check the method signature. Same name and same parameters = overriding. Same name, different parameters = overloading.
Next up: learn how to protect an object's internal state with Encapsulation.