What polymorphism means
Polymorphism, "many forms", is one piece of code doing different things depending on which subclass an object really is. Write animal.speak() once, and the version for a dog, a cat or any other animal runs automatically. It builds on the two previous ideas: inheritance gives the types a common parent, and overriding gives each one its own behavior.
Setting it up
The lesson uses a parent Animal and two children. Each part of the contract is explicit in Kotlin:
open class Animal {
open fun speak() {
println("...")
}
}
class Dog : Animal() {
override fun speak() {
println("Woof")
}
}
class Cat : Animal() {
override fun speak() {
println("Meow")
}
}
Animal is open so it can be extended, and its speak is open so it can be overridden. Both children say override. Unlike in some other languages, methods are not overridable by default in Kotlin, so without open on speak this setup would not compile.
Static type and dynamic type
A variable has a declared (static) type, fixed when the code is compiled, and the object it refers to has an actual (dynamic) type, known only at run time:
val a: Animal = Dog()
a.speak()
Here the declared type is Animal and the actual type is Dog. The compiler allows the call because Animal has speak(). At run time the call goes to the actual class, so it prints Woof.
The declared type also limits what you can call. If Dog had an extra function fetch(), then a.fetch() would fail with unresolved reference 'fetch'. To use it you check the type first: if (a is Dog) a.fetch(). Inside the if, Kotlin knows a is a Dog and allows the call.
Collections of mixed animals
The payoff comes with a collection typed as the parent:
val animals: Array<Animal> = arrayOf(Dog(), Cat())
for (a in animals) {
a.speak()
}
The loop prints Woof and then Meow. The loop only knows it holds animals; each object supplies its own speak. A function can do the same with a parameter:
fun chorus(animals: List<Animal>) {
for (a in animals) a.speak()
}
Now add class Cow : Animal() { override fun speak() { println("Moo") } }. Calling chorus(listOf(Dog(), Cat(), Cow())) prints Woof, Meow and Moo, and the function did not change. A subclass that does not override speak falls back to the parent's version and prints .... That is the real benefit: code that works today keeps working as the hierarchy grows.
Arrays versus lists
Kotlin's Array is invariant: an Array<Dog> is not an Array<Animal>, even though a Dog is an Animal. Assigning one to the other is a type mismatch error. A read-only List<Animal> does accept List<Dog>, which is one reason Kotlin code often prefers lists. The lesson's array is declared as Array<Animal> from the start, so it holds both kinds.
Branching on the type
Usually you let overriding do the choosing, so you do not branch on the type at all. When you do need to know the actual class, when with is checks reads well:
fun describe(a: Animal) = when (a) {
is Dog -> "a dog"
is Cat -> "a cat"
else -> "some animal"
}
For a Dog() this returns a dog, for a Cat() a cat, and for a plain Animal() some animal. Reach for this only when the behavior cannot live in an overridden function. Each new subclass would otherwise need another branch here.
The exercise
The classes are already defined. Step 1 builds animals, an Array<Animal> from arrayOf(Dog(), Cat()). Step 2 loops over it and calls speak():
open class Animal {
open fun speak() {
println("...")
}
}
class Dog : Animal() {
override fun speak() {
println("Woof")
}
}
class Cat : Animal() {
override fun speak() {
println("Meow")
}
}
class Solution {
fun mainFunction() {
val animals: Array<Animal> = arrayOf(Dog(), Cat())
for (a in animals) {
a.speak()
}
}
}
The output is Woof and then Meow, one per line.
Common mistakes
- Forgetting
open. Missing onAnimal, the subclasses fail withthis type is final. Missing onspeak, the overrides fail withis final and cannot be overridden. - Declaring the array as
Array<Dog>. It cannot hold aCat. Use the parent type. - Expecting a different order. Elements are visited in array order, so
Woofcomes first only becauseDog()is the first element. - Calling a subclass-only function through the parent type. Check with
isfirst.
Key takeaways
- Polymorphism lets one call run different code depending on the object's actual class.
- In Kotlin it needs
openclasses and functions plusoverride. - The declared type decides what you may call; the actual type decides what runs.
- Code written against the parent type keeps working when new subclasses are added.