Start learning free

LESSON 184 OF 344 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in Java: One Call, Many Behaviors

Lesson Read the full explanation

TL ; DR:

Polymorphism is when a single piece of code (like animal.speak()) does different things depending on which subclass the object actually is. You write the call once, and the right override runs automatically.


Animal[] animals = { new Dog(), new Cat() };
for (Animal a : animals) {
    a.speak();   // dispatches to Dog.speak then Cat.speak
}


Full lesson:

Polymorphism (Greek for "many forms") is what makes inheritance and interfaces actually useful at scale. Once Dog and Cat both inherit from Animal and override speak(), you can stop caring which one you're holding. Any code that takes an Animal and calls speak() works for both.

Why it matters

Imagine you're writing a function that prints what every animal in a zoo says:

void Roll(Animal[] animals) {
    foreach (var a in animals) a.Speak();
}

This function works for any current or future kind of animal. Add a Cow subclass tomorrow — this loop runs Cow.Speak() automatically, no edits needed. That's the win: code that works today and keeps working as the type hierarchy grows.

Static type vs dynamic type

The reference is typed as Animal. The object it actually points to is a Dog, or a Cat, etc. Polymorphism is the language dispatching the method call based on the dynamic type (what it is at runtime) rather than the static type (what the variable was declared as).

Notes by language

  • C# / C++ — the parent method must be virtual (and the override marked override) for runtime dispatch. Otherwise you get static dispatch and the wrong version runs.
  • Java / JS / Python — methods are virtual by default. Just override and it works.

Your task

Animal, Dog, and Cat are already defined, with the right overrides. Inside MainFunction, build a collection containing one Dog and one Cat (typed as Animal), then loop through it calling speak() on each.

Expected output:

Woof
Meow
STEP 1 OF 2
    Ctrl + Enter to check

    JAVA · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in Java, explained

    What polymorphism is

    Polymorphism, Greek for "many forms", means that one piece of code does different things depending on which subclass the object really is. You write a call such as animal.speak() once, and the right override runs for each object.

    It builds on the last two lessons. Once Dog and Cat both extend Animal and override speak(), code that holds an Animal no longer needs to care which kind it is.

    Why it matters

    Suppose you write a method that makes every animal in a zoo speak:

    void roll(Animal[] animals) {
        for (Animal a : animals) {
            a.speak();
        }
    }
    

    This method works for every animal type that exists today and for every one added later. If you create a Cow class tomorrow, with its own speak(), the loop runs it with no edits to roll. The code keeps working as the class hierarchy grows, and that is the main benefit.

    Static type and dynamic type

    Two types are involved in every call:

    • The static type is the type the variable was declared with. For Animal a it is Animal. The compiler uses it to check that speak() exists.
    • The dynamic type is the class of the object the variable actually refers to at runtime, such as Dog or Cat. Java uses it to choose which version of speak() runs.

    Choosing the method at runtime, from the object's real class, is called dynamic dispatch. In Java it applies to all instance methods unless they are final or private, so a plain override is enough and no keyword on the parent is needed.

    Parent-typed variables

    A subclass object can be stored in a variable of its parent type without any cast, because it is a kind of the parent:

    Animal pet = new Dog();
    pet.speak();
    

    Here pet has the static type Animal and refers to a Dog, so the call runs Dog's speak(). The reverse direction is stricter: through pet you can call only what Animal declares. A method that exists only in Dog, such as fetch(), is not visible there, and the compiler refuses the call.

    The lesson's exercise, step by step

    The starter code already defines Animal, whose speak() prints ..., and two subclasses, Dog and Cat. Each overrides speak(). The body of mainFunction is empty. You write two steps:

    1. Collect the animals. Declare an Animal[] named animals with an array initializer that holds new Dog() and new Cat(). Storing both under the parent type means the code that uses the array knows only that it holds animals.
    2. Loop and call. Use a for-each loop with an Animal variable a and call a.speak() inside it. The same call runs Dog's version first and Cat's version second.

    The full program:

    class Animal {
        public void speak() {
            System.out.println("...");
        }
    }
    
    class Dog extends Animal {
        @Override
        public void speak() {
            System.out.println("Woof");
        }
    }
    
    class Cat extends Animal {
        @Override
        public void speak() {
            System.out.println("Meow");
        }
    }
    
    class Solution {
        public void mainFunction() {
            Animal[] animals = { new Dog(), new Cat() };
            for (Animal a : animals) {
                a.speak();
            }
        }
    }
    

    It prints:

    Woof
    Meow
    

    The array's element type is Animal and the loop variable is Animal, yet the output shows the subclass behavior. Nothing in the loop mentions Dog or Cat. If you removed the overrides, both lines would print ..., the parent's version.

    Details worth knowing

    • The initializer in braces. Animal[] animals = { new Dog(), new Cat() }; is shorthand for creating an array of two elements and filling it. It is allowed only in a declaration. Elsewhere you write new Animal[] { ... }.
    • The array holds references. It doesn't contain whole objects. Each slot refers to an object, and the slots have the type Animal, so a slot can refer to any subclass object.
    • Checking the real class. The expression a instanceof Cat is true when the object behind a is a Cat. Needing it often is a sign the design should use an overridden method instead.
    • The same idea with interfaces. A variable can also have an interface type, and calls through it dispatch to the implementing class in the same way.
    • Fields don't behave this way. Dynamic dispatch applies to instance methods only. Reading a field uses the variable's static type.

    Common mistakes

    • Declaring the array as Dog[]. A Cat can't be stored in it. The element type has to be the common parent, Animal.
    • Calling a subclass-only method through the parent type. a.fetch() fails to compile if fetch is declared only in Dog.
    • Forgetting to override. If Cat misspelled speak, the loop would print the parent's ... for the cat. @Override turns that mistake into a compile error.
    • Using a static method. Static methods are not dispatched by the object's class, so a polymorphic method must be an instance method.
    • Calling speak() on the array. animals.speak() is an error. The call belongs on each element.

    Key takeaways

    • Polymorphism lets one call run different code depending on the object's actual class.
    • A variable of a parent type can refer to any subclass object.
    • The compiler checks the static type, and the runtime chooses the method from the dynamic type.
    • In Java, every instance method that is not final or private works this way, with no extra keyword.
    • Code written against the parent type keeps working when new subclasses are added.
    Keep going with JavaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free