Instruction

Override Method

Inheritance in Kotlin lets you create new classes, or subclasses, that inherit properties and behaviors from existing classes, or superclasses. But subclasses aren’t just copycats! They can add their own unique properties and methods, making them more specialized than their superclasses.

Imagine the Food Authority announces a new decree: all fruits must be labeled to indicate if they have stones or other hard parts. Here’s how you can use inheritance to implement this new rule:

  • You have a base class, Food, representing general food items.
  • You create a subclass, Fruit, that inherits all the properties and methods of Food.
  • Since fruits specifically can have stones, you add a new property, hasStone, to the Fruit class. This property wouldn’t apply to the general Food class.

In addition to adding unique properties, subclasses can override methods inherited from the superclass. This means providing a custom implementation for an existing method. Revisit the fruit labeling example:

  • The Food class has a generic label method that simply prints the food’s name and price.
  • In the Fruit subclass, you can override the label method to include information about the presence of a stone, adhering to the Food Authority’s regulation.
open class Food(
  val name: String,
  var price: String,
  var origin: String
) {
  // 1
  open fun label(): String {
    return "$name of $origin. Price: $price"
  }
}

class Fruit(
  name: String,
  price: String,
  origin: String,
  val stone: Boolean = false
) : Food(name, price, origin) {
  // 2
  fun hasStone(): Boolean {
    return stone
  }

  // 3
  override fun label(): String {
    val stonedLabel = if (hasStone()) "Stoned " else ""
    return "${stonedLabel}Fruit ${super.label()}"
  }
}

Here’s a code breakdown:

  • //1 The original label method that returns the name and price of the food item. You need to add an open modifier to declare that this method can be overridden. Since you plan to extend the Food class, use the open modifier. Both are required to make it work.
  • //2 This method classifies whether a fruit has a stone.
  • //3 The new method that returns an updated label only for fruits. Note that we call super.label() to use the label from the Food class at the end of our label.

Kotlin Playground to the rescue! It’s time to try this code in action. Add all the class definitions above and the following main() method:

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()) // Fruit Tomato of US. Price: 1.0
  
  val peach = Fruit("Peach", "2.0", "Chile", true)
  println(peach.label()) // Stoned Fruit Peach of Chile. Price: 2.0

}

Even though Fruit is a Food, as long as you created the correct instance of a class and assigned a variable of the same type, the method label would give you the correct Fruit label in line with the guidance from the Food Authority.

Benefits

Method overriding is a powerful tool that promotes code reusability, flexibility, and polymorphism, leading to well-structured and maintainable code.

Flexibility and Extensibility

Subclasses can specialize the behavior of inherited methods. This lets you create a generic foundation in the parent class and define specific variations in subclasses without modifying the parent class itself, promoting a more adaptable codebase.

Consider a simple example. You have a base class Fruit with a method describeColor(). This method is then overridden in the subclasses Peach and Plum to provide their own specific implementation:

open class Fruit {
  open fun describeColor() {
    println("Fruits can be of various colors.")
  }
}

class Peach : Fruit() {
  override fun describeColor() {
    println("A peach is usually pinkish or yellowish.")
  }
}

class Plum : Fruit() {
  override fun describeColor() {
    println("A plum is usually purple or reddish.")
  }
}

fun main() {
  val myFruit: Fruit = Fruit()
  val myPeach: Fruit = Peach()
  val myPlum: Fruit = Plum()

  myFruit.describeColor() // Fruits can be of various colors.
  myPeach.describeColor() // A peach is usually pinkish or yellowish.
  myPlum.describeColor() // A plum is usually purple or reddish.
}

Polymorphism

Overriding enables polymorphism. Polymorphism allows you to treat objects of different subclasses uniformly through a common interface, the parent class. When you call the overridden method on a parent class reference, the actual subclass implementation executes at runtime, leading to flexible behavior.

In the context of the Fruit example, you can create a function that takes a Fruit reference but can handle any specific fruit type, like Peach or Plum, and call the describeColor() method. The specific implementation of describeColor() called will depend on the actual object type at runtime.

Use the code above and add this method before the main() function:

fun describeFruitColor(fruit: Fruit) {
  fruit.describeColor()
}

Then add code this to main() methods:

  describeFruitColor(myPeach) // A peach is usually pinkish or yellowish.
  describeFruitColor(myPlum) // A plum is usually purple or reddish.

In this example, the describeFruitColor() accepts a Fruit reference, but it can handle any object that is a Fruit, including Peach and Plum. When you call describeFruitColor(), the specific describeColor() method called depends on the actual object type at runtime. This is polymorphism in action.

Code Reusability

You can reuse the general functionality from the parent class methods while customizing specific parts in subclasses. This avoids code duplication and promotes code maintainability.

This time, you’ll use the base class Tree with a grow() method. This method is then overridden in the subclasses PeachTree and PlumTree to provide their own specific implementation:

open class Tree {
  open fun grow() {
    println("The tree is growing.")
  }
}

class PeachTree : Tree() {
  override fun grow() {
    super.grow()
    println("The peach tree is blossoming with pinkish flowers.")
  }
}

class PlumTree : Tree() {
  override fun grow() {
    super.grow()
    println("The plum tree is blossoming with white flowers.")
  }
}

fun main() {
  val myTree: Tree = Tree()
  val myPeachTree: Tree = PeachTree()
  val myPlumTree: Tree = PlumTree()

  myTree.grow() // The tree is growing.
  myPeachTree.grow() // The tree is growing.
  //         The peach tree is blossoming with pinkish flowers.
  myPlumTree.grow() // The tree is growing.
  //         The plum tree is blossoming with white flowers.
}

The grow() method in the Tree class provides a generic behavior. The PeachTree and PlumTree classes override this method to reuse the code of generic behavior and add their own specific behaviors.

Override Constructor Properties

As you already know, overriding is a very powerful tool. You can use it not only for methods but also for overriding properties.

In Kotlin, constructors can’t be directly overridden in subclasses. However, you can achieve a similar effect for properties by overriding them within the subclass constructor. Here’s what you need to understand:

  • Properties: You can override properties declared with the open modifier in the superclass. This lets subclasses define their own behavior for accessing or modifying the property.
  • Constructors: Constructors aren’t directly overridden. Subclass constructors can call the superclass constructor using super() and then potentially override properties within the subclass constructor body.

Keep in mind:

  • Initialization Order: The superclass constructor and its initialization logic run before the subclass constructor. This means that overridden properties in the subclass might not be initialized yet when accessed within the superclass constructor.
  • val vs. var: You can override a val property with a var property in the subclass, but not the other way around.
  • Use Cases: Property overriding within the constructor is useful when a subclass needs to modify or calculate a property value based on its characteristics during object creation.

Now, create a new type of Food:

open class Food(open val name: String)

class LocalFood(name: String, val isLocal: Boolean): Food(name) {
  // Override the name property during initialization
  override val name = name.toUpperCase()
}

In the definition of a LocalFood, you used name: String as a constructor parameter, not the class property— it has no val or var prepended. Then, you passed name into the Food constructor, and the name property overrode.

LocalFood first calls the superclass constructor with the original name value. Then, it overrides the name property with its uppercase version.

Note: Be cautious when accessing overridden properties within the superclass constructor because they might not be initialized yet. If necessary, consider using custom getters or initialization logic in the subclass.

Rewrite the code to make it less ambiguous and add a new method:


class LocalFood(nameParam: String, val isLocal: Boolean): Food(nameParam) {
// Override the name property during initialization
  override val name = nameParam.toUpperCase()
  
  fun secretName(): String {
    return super.name
  }
}

Here, the original name parameter owned by the base class Food can still be accessed using a call to super. super.name is the same as you would call Food().name.

Add this code to the main function to verify everything you just learned:

fun main() {
    val food = LocalFood("Plum", false)
    println(food.name) // PLUM
    println(food.secretName()) // Plum
}

LocalFood.name refers to overridden LocalFood property .name, and LocalFood.secretName refers to Food.name and takes the value of name constructor parameter as is, without conversion.

Override Special Methods

Now, it’s time to discuss two methods responsible for ensuring equality between class instances: equals() and hashCode().

  • The equals() method is fundamentally important in Kotlin for defining object equality. It determines how two objects of the same class are considered equal.
  • The hashCode() method is also fundamental for working with objects in Kotlin collections like HashMap, HashSet, and others.

equals()

So, why is equals() important?

  • Default Behavior: By default, Kotlin’s equality operator (==) uses the equals() method to compare objects for structural equality. This means two objects are considered equal if their properties have the same values, regardless of whether they are the same object in memory.
  • Reference Equality: If you must compare objects based on whether they are the same object in memory - reference equality, Kotlin provides the === operator.

Overriding equals() has several benefits:

  • Collections and Hashing: Collections like HashSet and HashMap rely on the equals() method to determine an object’s uniqueness. A well-defined equals() ensures objects are correctly added, removed, and searched for within these collections.
  • Code Clarity: Overriding equals() explicitly defines how your objects should be compared for equality. This improves code readability and maintainability.
  • Consistent Behavior: A proper equals() implementation ensures consistent behavior when comparing objects throughout your code.

It’s time to get back to eating your Food:

// Overriding equals
open class Food(
  val name: String,
  var price: String,
) {

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is Food) return false
    //1
    return name == other.name
  }
}

class Fruit(
  name: String,
  price: String,
  val stone: Boolean = false
) : Food(name, price) {
  
  fun hasStone(): Boolean {
    return stone
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is Fruit) return false
    // 2
    return name == other.name && stone == other.stone
  }
}

In this code:

  • //1 Apart from the general check of “same class” and “not-null”, you check for equality of only the name of the Food, not the price because you know that price could change tomorrow.
  • //2 For the Fruit class, you add an additional check for the presence of stone.

Validation for the theory above looks like this:


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

  // 1
  println(tomato.equals(stonedtomato)) // true
  // 2
  println(stonedtomato.equals(tomato)) // false
}

Here, you:

  • //1 Call the equals method for the Food class, which compares only the name. Even though stonedtomato is a Fruit, it’s also a Food, hence true.
  • //2 Call the equals method for the Fruit class, which compares not only the name but also the presence of stone in the fruit. But the check for the correct class type fails because Food is not a Fruit, hence false.

hashCode()

Hashcode is mostly useful for Hash-Based Collections:

  • Hash-based collections rely on the hashCode() method to efficiently store and retrieve objects.
  • The hashCode() method should return a consistent integer value for an object based on its essential properties. This value determines the bucket where the object will be stored in the collection.
  • A well-defined equals() method ensures that objects with the same content end up in the same bucket, enabling efficient retrieval using the equals() method again during lookups.

Benefits of Overriding hashCode():

  • Correct Behavior in Collections: Class instances with the same content are treated as equal in sets and maps, leading to expected behavior like avoiding duplicates in sets.
  • Customizable Equality Logic: You define how objects should be compared for equality based on your specific needs.

Make sure when you’re adding a hashCode() function, it calculates the hash code based on the same properties as equals(). Otherwise, you might end up with unexpected behavior.

For example, if you add this method to the Food class:

  override fun hashCode(): Int {
    return 1 + price.hashCode()
  }

This hashCode() implementation is using only price property and since equals() method is checking equality only for the name property, when you try to add Food objects into set, you will get following.

Replace the main function with:

fun main() {
  val tomato = Food("Tomato", "1.0")
  val cucumber = Food("Tomato", "2.0")
  //1
  val foods = mutableSetOf<Food>()
  foods.add(tomato)
  //2
  println(cucumber in foods) // false
  //3
  println(cucumber == foods.first()) // true
  //4
  println(tomato == cucumber) // true
}

Here:

  • //1 tomato is added to a mutableSet, which uses a HashSet underneath.
  • //2 You verify if cucumber is present in the list: it is not.
  • //3 You check if the first element in the set is cucumber and the answer is true. This doesn’t make sense, but it’s because equals() checks only name equality, whereas the previous check was based on hashCode(). In this example, hashCode() is calculated based on the price property.
  • //4 you’re confirming that equals() using only name for comparison.
See forum comments
Download course materials from Github
Previous: Introduction Next: Demo