Start learning free

LESSON 188 OF 348 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in Kotlin: 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.


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

fun roll(animals: Array<Animal>) {
    for (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

    KOTLIN · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in Kotlin, explained

    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 on Animal, the subclasses fail with this type is final. Missing on speak, the overrides fail with is final and cannot be overridden.
    • Declaring the array as Array<Dog>. It cannot hold a Cat. Use the parent type.
    • Expecting a different order. Elements are visited in array order, so Woof comes first only because Dog() is the first element.
    • Calling a subclass-only function through the parent type. Check with is first.

    Key takeaways

    • Polymorphism lets one call run different code depending on the object's actual class.
    • In Kotlin it needs open classes and functions plus override.
    • 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.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free