Java Polymorphism

Polymorphism means "many forms." In Java it lets a single method call behave differently depending on the actual object that receives it. When you combine inheritance with overridden methods, you can write code that talks to a general type and still gets the specialized behavior of each subtype at runtime.

What polymorphism really means

Imagine a parent class called Animal with a method makeSound(). A Dog and a Cat both extend Animal, but a dog barks and a cat meows. Polymorphism is what allows you to hold any of these objects in an Animal variable and still hear the correct sound when you call makeSound(). The decision about which version runs happens while the program is executing, not while it is being compiled.

This matters because it lets you write flexible code. You can add a brand new subclass later, such as Cow, and the code that loops over a list of animals keeps working without a single change.

Overriding a parent method

class Animal {
  public void makeSound() {
    System.out.println("Some generic animal sound");
  }
}

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

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

public class Main {
  public static void main(String[] args) {
    Animal a = new Dog();
    a.makeSound(); // prints Woof
    a = new Cat();
    a.makeSound(); // prints Meow
  }
}
Note: The @Override annotation is optional, but always use it. If you misspell the method name or get the parameters wrong, the compiler will warn you instead of silently creating a brand new method.

One list, many types

The power of polymorphism shows up when you group different objects under a common parent type and treat them uniformly. The loop below does not know or care which specific animal it is dealing with. It simply asks each one to make its sound.

Looping over a mixed group

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

    for (Animal animal : zoo) {
      animal.makeSound();
    }
    // Output:
    // Woof
    // Meow
    // Woof
  }
}

Compile-time vs runtime polymorphism

Java has two flavors of polymorphism. Method overloading (same method name, different parameter lists in the same class) is resolved by the compiler and is often called compile-time polymorphism. Method overriding (a subclass replacing a parent method) is resolved while the program runs and is called runtime polymorphism. When people say "polymorphism" in an object-oriented sense, they usually mean the runtime kind.

FeatureOverloadingOverriding
Where methods liveSame classParent and child class
Method signatureMust differ (parameters)Must be identical
ResolvedAt compile timeAt runtime
Also calledStatic polymorphismDynamic polymorphism
Note: A useful mental model: overriding answers "which class's version of this method runs?" while overloading answers "which method with this name matches my arguments?"

Why this is worth learning

  • You write code against general types, so it stays open to new subclasses.
  • It removes long chains of if/else that check an object's exact type.
  • It is the foundation for design patterns and framework code you will meet later.
  • It keeps each subclass responsible for its own behavior, which is easier to maintain.

Exercise: Java Polymorphism

What does runtime polymorphism in Java rely on?