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
overridemodifier. Leaving it out is a compile error:method speak of type (): Unit needs `override` modifier. - Marking the parent's method
finalforbids overriding. The compiler then reportscannot 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.
- Build the collection. Declare
val animals: Array[Animal]and fill it withArray(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 isArray[Animal]. CallingDog()withoutnewis allowed in Scala 3 for classes. - Loop and call. A
for (a <- animals)loop visits each element, anda.speak()inside it printsWooffor the dog and thenMeowfor 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, likevoidin 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()andCat()and builds anArray[Animal]anyway. Writing the type states the intent. - Arrays are invariant. An
Array[Dog]can't be assigned to anArray[Animal]; the compiler reports a type mismatch. This is why the array is built asArray[Animal]from the start. An immutableList[Dog]would be accepted as aList[Animal]. for (a <- animals). The arrow reads "for eachainanimals". It walks the values directly, with no index.
Common mistakes
- Forgetting
override. Without it the subclass doesn't compile, becauseAnimal.speak()already has a body. - Typing the array as the subclass.
Array(Dog(), Cat())assigned to anArray[Dog]fails, since aCatis not aDog. - Swapping the order. The loop prints in array order, so
Array(Cat(), Dog())printsMeowfirst. The test expectsWoofand thenMeow. - Expecting subclass-only methods. Through an
Animalreference, onlyAnimal'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;finalprevents it. - Code written against the parent type works for every current and future subclass.
