Instruction

Introducing Inheritance

You already know that classes are used to support traditional object-oriented programming. Class concepts include inheritance, overriding, polymorphism and composition, which makes them suited for this purpose. Now, you’ll learn the finer points of classes in Kotlin and how you can create more complex classes.

Note: Per usual, you’ll use Kotlin Playground - as linked below - for all the coding examples: Kotlin Playground

In previous lessons, you used the Food class extensively. Now, it’s time to create the Fruit class. Don’t copy this code yet. Just inspect and understand how it works. Take a look at these classes in depth again:

class Food(
  val name: String,
  var price: String,
  var origin: String)

class Fruit(
  val name: String,
  var price: String,
  var origin: String,
  val stone: Boolean,
) {
  fun hasStone():Boolean {
    return stone
  }
}

As you can see in the code above, there’s a lot of duplication between Food and Fruit. The properties are almost the same, which isn’t surprising since Fruit is also a type of Food! Add the code below to your Kotlin Playground:

// Fruit is a Food
open class Food(
  val name: String,
  var price: String,
  var origin: String) {

  fun label(): String {
    return "$name of $origin. Price: $price"
  }
}

The Food class now has the open modifier.

Have you ever noticed how all apples are fruit, but not all fruit are apples? This real-world connection is perfectly reflected in a powerful concept called class inheritance.

Inheritance is powerful, but it requires clear instructions. In Kotlin, classes by default can’t be inherited from unless explicitly marked as open for extension. The open keyword applied to the Food class here makes it inheritable.

Imagine a fruit basket overflowing with deliciousness. You can represent this in code with a base class named Food. But wait, there’s more!

Not all food is created equal. You can create a subclass named Fruit that inherits from the general Food class. Like an apple inherits its sweetness from the broader category of fruits, the Fruit class inherits properties and behaviors from the Food class.

This code example demonstrates the essence of inheritance.

Now, add this code after the previous code and observe:

class Fruit(
  name: String,
  price: String,
  origin: String,
  val stone: Boolean = false
): Food(name, price, origin)

As you can see, you don’t have a main function yet, so nothing will come up when you run the program. What you’ve added above is now a class with a : Food statement, which shows how the Fruit class now inherits from Food, indicated by a colon after defining the stone property. This is followed by the class from which Fruit inherits, which, in this case, is Food.

The beauty of inheritance shines in code reusability. The Fruit class automatically gains access to all the properties and methods defined in its parent class by inheriting from’ Food’. In other words, a Fruit truly is a Food in the code’s world. This eliminates the need to duplicate code, keeping things clean and efficient.

Imagine you have several general food properties like name, price, and origin. Creating a Food class with these properties means you don’t have to rewrite them for every fruit type. The Fruit class inherits them all! This not only saves you time and effort but also ensures consistency across your codebase.

Note: When a subclass inherits from a parent class, it’s also inheriting the parent’s constructor. So, if the Food class has a constructor that expects specific parameters, such as name, price, and origin, you must provide those arguments when creating a Fruit object. Essentially, the Fruit constructor must pass along the required information to the Food constructor.

Now, you’ll expand on more features that you can use to differentiate Fruit qualities, such as having a stone. Add the following code to the previously added code:

 {
  fun hasStone(): Boolean {
    return stone
  }
}

You’ve added the method hasStone, which shows how the previous class differs from this one. Like in the real world, you can tell whether a certain fruit has a stone. For example, if someone asks for a peach, you automatically know it has a stone. But if they just ask for food or fruit, you don’t know if it has a stone. Does that make sense?

Similarly, hasStone provides a program-discernible quality to determine whether the fruit has a stone.

Now, add the main function to your program:

fun main() {
  val tomato = Food("Tomato", "1.0", "US")
  val tomato2 = Fruit("Tomato", "1.0", "US")

  println(tomato.label()) //Tomato of US. Price: 1.0
  println(tomato2.label()) //Tomato of US. Price: 1.0
}

Now you can run this! It should print the specified results. Both tomato and tomato2 have all the properties of a Food because Fruit inherits from Food. Additionally, only the Fruit object will have all the properties and methods defined in Fruit.

Next, add this to the end of your current code:

println(tomato.hasStone()) // Error: Unresolved reference: hasStone

You’ll immediately get an error, but if you replace it with the following line, it’ll work flawlessly:

  println(tomato2.hasStone()) // false

As you can see, even though Fruit is a Food, the Food doesn’t have a hasStone function because the hierarchy only goes one way: all fruits are food, but not all food is a fruit. You know, there’s more to food than just being fruit. You can also have vegetables, as you’ll initiate in the next examples.

Polymorphism

Have you ever noticed how a chameleon can adapt its appearance to blend into its surroundings? Polymorphism in Kotlin is like a chameleon for your code! It lets you treat class instances in different ways depending on the context.

In the example, Fruit is a subclass of Food. This means a Fruit inherits all the properties and behaviors of Food. But polymorphism takes things a step further.

Imagine writing code that can handle any food, from fruits to vegetables. With polymorphism, you can write a function that works with both Food and Fruit classes, even though they are technically different.

Behind the scenes, Kotlin figures out the exact type of object being used and calls the appropriate methods. This makes your code more flexible and reusable.

Inheritance is a cornerstone of object-oriented programming, allowing you to create new classes, or subclasses, that inherit properties and behaviors from existing classes, or superclasses. Imagine a family tree where a child inherits traits from its parents. In code, this translates to a subclass inheriting from a superclass, forming a class hierarchy. Just like you might call a superclass a parent class and a subclass a child class, these terms are interchangeable.

Here are some key points to remember about inheritance in Kotlin:

  • Single Inheritance: A subclass can inherit from only one direct superclass. This keeps things organized and avoids complexity.
  • Open for Inheritance: Kotlin classes are not open for inheritance by default. You must explicitly mark a class with the open keyword to allow subclasses to inherit from it.
  • Multi-Level Inheritance: You can create a chain of inheritance where a subclass inherits from another subclass. This allows for specialization and code reuse across multiple levels.

By understanding these rules, you can effectively leverage inheritance to create well-structured and reusable code in your Kotlin projects.

Next, you’ll look at an example with polymorphism and a recently learned open class - does this look familiar? Copy it into a blank code file on the Kotlin Playground. It won’t run yet, so observe:

// Polymorphism
open class Food(
  val name: String,
  var price: String,
  var origin: String) {

  fun label(): String {
    return "$name of $origin. Price: $price"
  }
}

As seen before, the first part of this code ‘opens’ the new food class, reinstating some of the usual variables you’ve encountered before. Now paste this next bit under the previous code:

class Fruit(
  name: String,
  price: String,
  origin: String,
  val stone: Boolean = false
): Food(name, price, origin) {
  fun hasStone(): Boolean {
    return stone
  }
}
class Veg(
  name: String,
  price: String,
  origin: String,
  val rooted: Boolean = false
): Food(name, price, origin) {
  fun isRooted(): Boolean {
    return rooted
  }
}
fun foodLabel(food: Food) : String {
  return "Label: ${food.label()}"
}

The vegetables have finally entered the chat!

As you can see, because both Fruit and Veg derive from the Food class, it’s a valid input into the function foodLabel. More importantly, the function has no idea that the object passed in is anything other than regular Food. It can only observe the elements of Veg and Fruit, defined in the Food base class above.

Paste in the main function to this example, and run:

fun main() {
  val tomato = Fruit("Tomato", "1.0", "US")
  val carrot = Veg("The Carrot", "2.0", "Canada", true)

  println(foodLabel(tomato)) // Label: Tomato of US. Price: 1.0
  println(foodLabel(carrot)) // Label: The Carrot of Canada. Price: 2.0
}

With foodLabel, you see that both classes inherit the method, which has the same behavior. In the next lessons, you’ll see how to modify behaviors based on the class types and their parameters.

Super

In Kotlin, when you override a method in a subclass, you can leverage the super keyword to interact with the superclass’s version of the method. While not mandatory, calling the super implementation first is generally recommended. This ensures two key benefits:

  • Preserving Superclass Behavior: By calling super, you guarantee that any core functionality defined in the superclass’s method still executes. This prevents unintended side effects introduced by your subclass’s override.
  • Loose Coupling: When you call super, your subclass doesn’t need to know the specific details of the superclass’s implementation. The super keyword handles the interaction, keeping your code cleaner and more maintainable.

You’ll learn more about this in the next lesson with examples.

This is all for today’s lesson! Congrats, you have a more advanced understanding of not just tomatoes but other fruits and vegetables and their qualities.

Wanna hear a joke about pizza? Never mind, it’s too… cheesy.

See forum comments
Download course materials from Github
Previous: Introduction Next: Demo