Lesson 4

Inheritance and Polymorphism in Kotlin: Bringing Classes to Life

Introduction

Greetings! Today, we're going to demystify the crucial terms of Object-oriented programming (OOP): Inheritance and Polymorphism. These concepts form the backbone of efficient OOP. Our journey will unfold as follows: we'll start with an intuitive grasp of Inheritance and its implementation in Kotlin, and then we'll delve into Polymorphism, with a special focus on method overriding.

Understanding Inheritance

Inheritance is akin to repurposing an old blueprint to create something new. In OOP, it allows one class to inherit features (properties and functions) from another.

In Kotlin, we have the Parent Class (also known as Superclass), which provides features, and the Child Class (or Subclass), which receives these features. When implementing, we use the open keyword for the Parent Class (to allow inheritance), and the : symbol for the Child class indicates which Parent class the features are coming from.

Kotlin
1// Defining an 'Animal' class (Parent Class). Marking it as 'open' allows other classes to inherit from it. 2open class Animal { 3 fun eat() { 4 println("This animal eats food.") // Prints: "This animal eats food." 5 } 6} 7 8// 'Cat' class inherits from 'Animal' (Child Class) 9class Cat : Animal() { 10 // Inherits all features from 'Animal' 11} 12 13val myCat = Cat() 14myCat.eat() // Prints: "This animal eats food."
Handling Constructors in Inheritance

Inheritance in Kotlin involves subclasses inheriting features from their superclass. A crucial part of this process is ensuring that constructors in the superclass are properly invoked by the subclass. Constructors are special methods used to initialize new objects, and when a class inherits from another, the subclass must initialize the superclass as well, often providing the necessary parameters for any superclass constructor.

When a subclass inherits from a superclass, and both have primary constructors, the subclass's primary constructor needs to directly invoke the superclass's constructor. This ensures that any initial setup required by the superclass is performed:

Kotlin
1open class Animal(val name: String) { 2 init { 3 println("$name is an Animal.") 4 } 5} 6 7class Cat(name: String) : Animal(name) { 8 init { 9 println("$name is also a Cat.") 10 } 11} 12 13fun main() { 14 val myCat = Cat("Whiskers") 15 // Output: 16 // Whiskers is an Animal. 17 // Whiskers is also a Cat. 18}

In the above example, the Cat class inherits from Animal, and both classes have primary constructors that accept a name parameter. The Cat class passes this parameter to the Animal class's constructor using the : symbol followed by Animal(name), ensuring proper initialization.

For classes with secondary constructors, Kotlin requires that they either directly call the superclass constructor using super or delegate to another constructor in the same class that does:

Kotlin
1open class Animal(val name: String) 2 3class Cat : Animal { 4 constructor(name: String, lives: Int) : super(name) { 5 println("$name has $lives lives.") 6 } 7} 8 9fun main() { 10 val myCat = Cat("Felix", 9) 11 // Output: Felix has 9 lives. 12}

This setup, where a Cat class with a secondary constructor calls the superclass Animal constructor to ensure proper initialization, demonstrates the flexibility and power of Kotlin's inheritance mechanism. Understanding and applying constructor invocation correctly is essential for creating fully initialized, functional subclass objects in Kotlin, seamlessly extending the capabilities of the superclass.

Concept of Method Overriding in Inheritance

Method overriding allows a Child Class to provide unique implementations for methods provided by its Parent Class. In Kotlin, to indicate that a method in the Parent Class can be overridden, we use the open keyword. In the Child Class, we use the override keyword before the method that is intended to provide a new implementation:

Kotlin
1open class Animal { 2 open fun eat() { 3 println("This animal eats food.") 4 } 5} 6 7class Cat : Animal() { 8 override fun eat() { 9 println("The cat eats fish.") // A custom message for the Cat class 10 } 11} 12val myAnimal = Animal() 13myAnimal.eat() // Prints: "This animal eats food." 14val myCat = Cat() 15myCat.eat() // Prints: "The cat eats fish."

When an instance of Animal and Cat call their eat() methods, they print "This animal eats food." and "The cat eats fish." respectively, showcasing how subclasses can customize inherited behavior.

Exploring Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass, enabling a method to perform differently based on the object it operates on. This concept, pivotal in Object-Oriented Programming (OOP), leverages inheritance and method overriding for dynamic behavior at runtime.

To see polymorphism in action, consider an example with animals and their eating habits:

Kotlin
1open class Animal { 2 open fun eat() { 3 println("This animal eats food.") 4 } 5} 6 7class Cat : Animal() { 8 override fun eat() { 9 println("The cat prefers fish.") 10 } 11} 12 13class Dog : Animal() { 14 override fun eat() { 15 println("The dog enjoys bones.") 16 } 17} 18 19fun main() { 20 val animals = arrayOf(Cat(), Dog()) // Array of Animal type 21 22 for (animal in animals) { 23 animal.eat() // Executes overridden eat method 24 } 25 // Output shows specific eating preferences: 26 // The cat prefers fish. 27 // The dog enjoys bones. 28}

In this example, Cat and Dog override the eat method from Animal. Iterating over an array of Animal objects calls the specific eat method for each, illustrating polymorphism: the same operation (eat) varies by the object type, even when accessed through a reference of the superclass. Polymorphism facilitates flexible and reusable code, allowing operations on more general types while ensuring the correct specific behaviors are invoked.

Lesson Summary and Looking Forward

So far, so good! We have discovered Inheritance and Polymorphism in Object-oriented programming. We began by understanding Inheritance, which enables child classes to inherit from parent classes. Then, we looked at method overriding, which allows child classes to provide specific implementations of methods. Lastly, we learned about Polymorphism, which enables multiple behaviors to emerge from a single function.

Now, it's time for some hands-on practice! Practice can reinforce learning and help you become more comfortable with new concepts. Let's roll up our sleeves and dive in!

Enjoy this lesson? Now it's time to practice with Cosmo!

Practice is how you turn knowledge into actual skills.