Start learning free

LESSON 191 OF 348 · OBJECT-ORIENTED PROGRAMMING

OOP Capstone in Kotlin: Abstract Class and Polymorphism

Lesson Read the full explanation

TL ; DR:

Put it all together: an abstract parent class, three concrete subclasses, and a polymorphic loop that dispatches by runtime type. This is the shape of nearly every real OOP design.


abstract class Animal(val name: String) {
    abstract fun sound(): String      // each subclass picks
    fun speak() {                     // shared by all
        println("$name: ${sound()}")
    }
}
// Dog, Cat, Bird each override sound()
// val zoo = arrayOf(Dog("Rex"), ...)
// for (a in zoo) a.speak()


Full lesson:

This problem stitches together everything you've learned in this chapter:

  1. Inheritance — three concrete classes share a common parent.
  2. Constructors — each subclass forwards a name to the parent.
  3. Abstract methods — the parent declares sound() with no body; each child must provide one.
  4. Shared behavior — the parent has a concrete speak() that calls sound(); every subclass inherits it for free.
  5. Polymorphism — a collection typed as Animal holds a Dog, a Cat, and a Bird; speak() dispatches to the right sound() at runtime.

The shape of this code is the shape of nearly every real OOP design you'll see — a small contract at the top (the abstract parent), concrete variants below, and the rest of the program written against the abstract type. New variants get added without touching the consumer code.


Your task

The abstract Animal class is already defined, with a concrete speak() that calls the abstract sound(). Add three subclasses — Dog (returns "Woof"), Cat (returns "Meow"), and Bird (returns "Tweet"). Then in MainFunction / main, build a polymorphic collection holding one of each (Rex, Whiskers, Tweety in that order) and loop through it calling speak().

Expected output:

Rex: Woof
Whiskers: Meow
Tweety: Tweet
STEP 1 OF 2
    Ctrl + Enter to check

    KOTLIN · OBJECT-ORIENTED PROGRAMMING

    Oop Capstone in Kotlin, explained

    What the capstone puts together

    This exercise combines the pieces of the object-oriented chapter into one small design: an abstract parent class, three concrete subclasses, and a loop that treats them all as the parent type. The parent is Animal. It has a name, an abstract function sound() that each subclass must define, and a concrete function speak() that prints the name and the sound.

    The program has to print three lines, one per animal, in a fixed order:

    Rex: Woof
    Whiskers: Meow
    Tweety: Tweet

    How the parent class works

    The lesson gives you Animal:

    abstract class Animal(val name: String) {
        abstract fun sound(): String
        fun speak() {
            println("$name: ${sound()}")
        }
    }
    
    • val name: String in the constructor declares a parameter and a read-only property in one go. Every subclass inherits name.
    • abstract fun sound(): String has no body. Every concrete subclass has to implement it.
    • speak() is a normal function. Inside the string, $name inserts the property and ${sound()} inserts the result of a call. speak() doesn't know which animal it belongs to, yet it calls sound() and gets the right answer. That works because the call is resolved by the real type of the object at run time.

    This is the shape that gives the design its value: the shared behaviour (speak()) is written once, and the part that varies (sound()) is left to the subclasses.

    Step by step

    1. Write the three subclasses. Each one takes a name: String in its primary constructor. This parameter is a plain constructor parameter, without val, because the property already exists in Animal. A second val would try to declare a new property with the same name.
    2. Forward the name to the parent. After the colon, write Animal(name). This calls the parent's constructor with the argument. If you wrote Animal() instead, the compiler would report that no value was passed for parameter name.
    3. Override sound(). Use override fun sound(): String and return "Woof", "Meow" or "Tweet". You only override sound. The inherited speak() does the printing.
    4. Build the collection in mainFunction. arrayOf(Dog("Rex"), Cat("Whiskers"), Bird("Tweety")) creates an array of the three objects, in the order the output needs. Kotlin has no new: calling the class name creates the object. The declared type Array<Animal> says the array holds animals of any kind.
    5. Loop and call speak(). for (a in animals) visits the objects in order. Each a is treated as an Animal, and a.speak() prints its line.

    The solution in Kotlin

    abstract class Animal(val name: String) {
        abstract fun sound(): String
        fun speak() {
            println("$name: ${sound()}")
        }
    }
    
    class Dog(name: String) : Animal(name) {
        override fun sound(): String { return "Woof" }
    }
    
    class Cat(name: String) : Animal(name) {
        override fun sound(): String { return "Meow" }
    }
    
    class Bird(name: String) : Animal(name) {
        override fun sound(): String { return "Tweet" }
    }
    
    class Solution {
        fun mainFunction() {
            val animals: Array<Animal> = arrayOf(Dog("Rex"), Cat("Whiskers"), Bird("Tweety"))
            for (a in animals) {
                a.speak()
            }
        }
    }
    

    Kotlin details worth noting:

    • Polymorphism. The loop variable has type Animal, but each call runs the subclass's sound(). The loop never mentions Dog, Cat or Bird.
    • Arrays are invariant. An Array<Dog> is not an Array<Animal>. For example, val dogs = arrayOf(Dog("a")) followed by val animals: Array<Animal> = dogs gives initializer type mismatch: expected 'Array<Animal>', actual 'Array<Dog>'. With three different subclasses in the call, Kotlin infers Array<Animal> anyway, and the explicit type states the intention.
    • The name is read-only. val creates a property without a setter, so a.name = "x" doesn't compile.
    • Short form. A one-line override can be written override fun sound() = "Woof", with the return type inferred as String. The block form with an explicit type is equivalent.

    Adding a new animal

    The main benefit of this design appears when the program grows. To add a cow, write one more subclass:

    class Cow(name: String) : Animal(name) {
        override fun sound() = "Moo"
    }
    

    Put Cow("Bessie") in the array and the same loop prints Bessie: Moo. The loop and speak() don't change. The code that uses the abstract type stays untouched while new variants appear.

    Common mistakes

    • Using val in the subclass parameter. That conflicts with the inherited property.
    • Calling Animal() without the name. The parent constructor needs one argument.
    • Leaving out override. The compiler rejects a sound() that hides the parent's abstract function without the modifier.
    • Overriding speak() instead of sound(). speak() is final, because members are final unless marked open. The lesson only needs sound().
    • Wrong order in the array. The output follows the order of the elements: Rex, Whiskers, Tweety.
    • Wrong text. The sounds are Woof, Meow and Tweet, and the line format is name: sound with one space after the colon.

    Key takeaways

    • An abstract class holds the shared code and declares what subclasses must supply.
    • A subclass passes constructor arguments to its parent with : Animal(name).
    • Code written against the parent type works with every subclass, including ones added later.
    • Calls on the parent type run the subclass's override.
    Keep going with KotlinAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free