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: Stringin the constructor declares a parameter and a read-only property in one go. Every subclass inheritsname.abstract fun sound(): Stringhas no body. Every concrete subclass has to implement it.speak()is a normal function. Inside the string,$nameinserts the property and${sound()}inserts the result of a call.speak()doesn't know which animal it belongs to, yet it callssound()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
- Write the three subclasses. Each one takes a
name: Stringin its primary constructor. This parameter is a plain constructor parameter, withoutval, because the property already exists inAnimal. A secondvalwould try to declare a new property with the same name. - Forward the name to the parent. After the colon, write
Animal(name). This calls the parent's constructor with the argument. If you wroteAnimal()instead, the compiler would report that no value was passed for parametername. - Override
sound(). Useoverride fun sound(): Stringand return"Woof","Meow"or"Tweet". You only overridesound. The inheritedspeak()does the printing. - 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 nonew: calling the class name creates the object. The declared typeArray<Animal>says the array holds animals of any kind. - Loop and call
speak().for (a in animals)visits the objects in order. Eachais treated as anAnimal, anda.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'ssound(). The loop never mentionsDog,CatorBird. - Arrays are invariant. An
Array<Dog>is not anArray<Animal>. For example,val dogs = arrayOf(Dog("a"))followed byval animals: Array<Animal> = dogsgivesinitializer type mismatch: expected 'Array<Animal>', actual 'Array<Dog>'. With three different subclasses in the call, Kotlin infersArray<Animal>anyway, and the explicit type states the intention. - The name is read-only.
valcreates a property without a setter, soa.name = "x"doesn't compile. - Short form. A one-line override can be written
override fun sound() = "Woof", with the return type inferred asString. 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
valin 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 asound()that hides the parent's abstract function without the modifier. - Overriding
speak()instead ofsound().speak()is final, because members are final unless markedopen. The lesson only needssound(). - Wrong order in the array. The output follows the order of the elements: Rex, Whiskers, Tweety.
- Wrong text. The sounds are
Woof,MeowandTweet, and the line format isname: soundwith 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.