Start learning free

LESSON 406 OF 414 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in Scala: Overriding and Dynamic Dispatch

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.


val animals: Array[Animal] = Array(Dog(), Cat())
for (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:

def roll(animals: Array[Animal]): Unit = {
    for (a <- 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 for Scala

  • In Scala, methods can be overridden unless they are marked final, so no virtual keyword is needed. The child marks its version with override and the runtime dispatch just 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

    SCALA · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in Scala, explained

    What polymorphism means

    Polymorphism means "many forms". In object-oriented code it describes one call that behaves differently depending on the object it is made on. You write a.speak() once, and the method that runs is the one belonging to the object's real class.

    The pieces it needs are an inheritance relationship and an overridden method. In the lesson, Dog and Cat both extend Animal and both replace its speak(). Code that handles an Animal never has to ask which kind it holds.

    Static type and dynamic type

    A variable has a type written in the source, called the static type, and the object it refers to has a class, the dynamic type. They can differ:

    val d: Animal = Dog()
    d.speak()   // Woof
    

    The compiler checks the call against the static type: Animal has a speak() method, so the call is allowed. At run time the JVM looks at the actual object, a Dog, and runs Dog's version. That choice at run time is called dynamic dispatch.

    The static type also limits what you may call. If Dog had an extra method fetch(), then d.fetch() would not compile, and the compiler reports value fetch is not a member of Animal. Only members declared on Animal are visible through an Animal variable.

    Overriding in Scala

    Scala methods can be overridden unless they are marked final. Two rules matter:

    • A method that replaces an inherited concrete method needs the override modifier. Leaving it out is a compile error: method speak of type (): Unit needs `override` modifier.
    • Marking the parent's method final forbids overriding. The compiler then reports cannot override final member method speak.

    There is no virtual keyword to remember. Every method that isn't final already dispatches on the dynamic type.

    Why it is useful

    A function written against the parent type keeps working when the hierarchy grows:

    class Animal { def speak(): Unit = println("...") }
    class Dog extends Animal { override def speak(): Unit = println("Woof") }
    class Cat extends Animal { override def speak(): Unit = println("Meow") }
    class Cow extends Animal { override def speak(): Unit = println("Moo") }
    class Plain extends Animal
    
    def roll(animals: Seq[Animal]): Unit = {
      for (a <- animals) a.speak()
    }
    
    roll(List(Dog(), Cat(), Cow(), Plain()))
    

    The output is:

    Woof
    Meow
    Moo
    ...
    

    roll was never edited for Cow. Plain doesn't override anything, so it inherits Animal's version and prints ....

    The exercise, step by step

    The starter code already defines Animal, Dog and Cat, with the overrides in place. The work happens inside mainFunction, in two steps.

    1. Build the collection. Declare val animals: Array[Animal] and fill it with Array(Dog(), Cat()). The type annotation is the key: the array is typed by the parent, so the code that uses it only knows it holds animals. Scala writes type parameters in square brackets, so it is Array[Animal]. Calling Dog() without new is allowed in Scala 3 for classes.
    2. Loop and call. A for (a <- animals) loop visits each element, and a.speak() inside it prints Woof for the dog and then Meow for the cat. The call is identical on both iterations; the objects differ.

    The solution in Scala

    class Animal {
        def speak(): Unit = {
            println("...")
        }
    }
    
    class Dog extends Animal {
        override def speak(): Unit = {
            println("Woof")
        }
    }
    
    class Cat extends Animal {
        override def speak(): Unit = {
            println("Meow")
        }
    }
    
    class Solution {
        def mainFunction(): Unit = {
            val animals: Array[Animal] = Array(Dog(), Cat())
            for (a <- animals) {
                a.speak()
            }
        }
    }
    

    Some details of this code:

    • Unit. It is the return type of methods that exist for their effect, like void in other languages. The last expression of the body would normally be the result, but here the body only prints.
    • The annotation is optional but informative. Without it, Scala infers the common parent type from Dog() and Cat() and builds an Array[Animal] anyway. Writing the type states the intent.
    • Arrays are invariant. An Array[Dog] can't be assigned to an Array[Animal]; the compiler reports a type mismatch. This is why the array is built as Array[Animal] from the start. An immutable List[Dog] would be accepted as a List[Animal].
    • for (a <- animals). The arrow reads "for each a in animals". It walks the values directly, with no index.

    Common mistakes

    • Forgetting override. Without it the subclass doesn't compile, because Animal.speak() already has a body.
    • Typing the array as the subclass. Array(Dog(), Cat()) assigned to an Array[Dog] fails, since a Cat is not a Dog.
    • Swapping the order. The loop prints in array order, so Array(Cat(), Dog()) prints Meow first. The test expects Woof and then Meow.
    • Expecting subclass-only methods. Through an Animal reference, only Animal's members can be called.

    Key takeaways

    • The method that runs depends on the object's class at run time, not on the variable's declared type.
    • Overriding a concrete method requires override; final prevents it.
    • Code written against the parent type works for every current and future subclass.
    Keep going with ScalaAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free