Kotlin Overriding Methods

Method overriding is an important part of polymorphism. When we override methods, we are redefining the behavior of a base class method in a child class. Doing so allows us to create specialized cases of our classes that still work with existing code. At runtime, the JVM will use the proper implementation of a method depending on the object’s type.

Kotlin has it’s own twist on overriding classes and methods. OOP has an issue known as “fragile super-classes” where changes in base classes break child classes due to developers not fully considering how changes in a base class may affect a child class. Due to the fragile super-class issue, Kotlin disables both inheritance and method overriding by default. In order to use either, we must signal to the compiler that we can extend a class or override its methods by using the open keyword. Open classes and methods force developers to consider the possibility that a class may be extended or it’s methods or overridable.

Here is an example of an open class.

/**
 * Base class. This class has to be marked as open to allow
 * inheritance in Kotlin
 */
open class GrillCook {

    /**
     * We also have to mark our methods as open to allow
     * for overriding
     */
    open fun print() {
        println("Grill Cook is the Master")
    }
}

Our GrillCook class can be extended because we have included the open keyword in front of the class keyword. Likewise, our print() method is also marked as open can be therefore overridden by child classes. So our next class, BobBelcher takes advantage of the opportunity.

/**
 * Child class that override print in the base class. Notice that this
 * class declares it's name property as open, which means child classes
 * can override the property also
 */
open class BobBelcher(open val name : String) : GrillCook(){

    //override keyword signals that we are overriding a super class method
    override fun print(){
        println("$name is the Master")
    }
}

BobBelcher overrides the print() method and changes what is printed to the console. Since this class has a name property, we print “$name is the Master” rather than “Grill Cook is the Master”. To override print(), Bob has to have the keyword override in the method signature. This is to prevent another common OOP problem where developers intended to override a function but accidentally overloaded it instead, usually due to some sort of typo.

Kotlin supports property overriding if properties are marked as open. The name property on BobBelcher is marked as open, which means child classes can also override the name property. This is what LindaBelcher does.

/**
 * LindaBelcher is a child class of BobBelcher and overrides the
 * name property to set it to Linda rather than some other string
 */
class LindaBelcher : BobBelcher("") {
    override val name = "Linda" //Use the override keyword to override a property

    override fun print() {
        //Use the print implementation found in BobBelcher
        super.print()
        println("Alright!!!")
    }
}

LindaBelcher overrides the name property found in BobBelcher and returns Linda. That means that no matter what String gets passed to the BobBelcher constructor invocation in LindaBelcher, we end up getting “Linda” when we use the name property. It’s also worth noting that Linda has a call to super.print() inside of the print() function. Using the super.print() tells the compiler to call the implementation of print() found in Bob. Then Linda’s print function completes by printing “Alright!!!” to the console.

Putting it Together

Here is a complete program that demonstrates method overriding in Kotlin.

package ch1.overriding

/**
 * Base class. This class has to be marked as open to allow
 * inheritance in Kotlin
 */
open class GrillCook {

    /**
     * We also have to mark our methods as open to allow
     * for overriding
     */
    open fun print() {
        println("Grill Cook is the Master")
    }
}

/**
 * Child class that override print in the base class. Notice that this
 * class declares it's name property as open, which means child classes
 * can override the property also
 */
open class BobBelcher(open val name : String) : GrillCook(){

    override fun print(){
        println("$name is the Master")
    }
}

/**
 * LindaBelcher is a child class of BobBelcher and overrides the
 * name property to set it to Linda rather than some other string
 */
class LindaBelcher : BobBelcher("") {
    override val name = "Linda"

    override fun print() {
        //Use the print implementation found in BobBelcher
        super.print()
        println("Alright!!!")
    }
}

fun main(args : Array<String>){
    val grillCook = GrillCook()
    val bob = BobBelcher("Bob")
    val linda = LindaBelcher()

    println("Using grillCook::print")
    grillCook.print()

    println("\nUsing BobBelcher::print")
    bob.print()

    println("\nUsing LindaBelcher::print")
    linda.print()
}

This is the output when run.

Using grillCook::print
Grill Cook is the Master

Using BobBelcher::print
Bob is the Master

Using LindaBelcher::print
Linda is the Master
Alright!!!

It works because at runtime, the JVM knows to use the proper implementation of print() based on the objects type at runtime. This is why all three objects print different outputs when print is called.

References

https://kotlinlang.org/docs/reference/classes.html

Advertisement

Kotlin Covariant Return types

It’s legal to substitute a child class as the return type of a base class when overriding a function. Doing so allows us to avoid unnecessary casting. Let’s look at an example.

abstract class Cook(val name : String) {

    //Abstract method that returns Cook
    abstract fun copy() : Cook
}

class Bob(name : String) : Cook(name) {

    /**
     * Notice how this method changes the
     * return type from Cook to Bob. This is legal
     * because Bob is a child class of Cook.
     *
     * This is called covariant return types
     */
    override fun copy(): Bob {
        return Bob(this.name)
    }
}

In this example, we have a base class Cook that has an abstract copy() method. The copy() method returns a Cook object. However, Bob overrides copy() and returns Bob. The difference may look small, but when used we do not need to downcast Cook objects back to Bob.

fun main(args : Array<String>){
    val bob = Bob("Bob")

    //No casting required since copy() returns a Bob
    val bobCopy : Bob = bob.copy()
    val cookCopy : Cook = bob.copy()

    println(bob.name)
    println(bobCopy.name)
    println(cookCopy.name)
}

If we had the copy() method in Bob return Cook rather than Bob, we would need to down cast.

class Bob(name : String) : Cook(name) {

    //This verion returns the Base Type
    override fun copy(): Cook {
        return Bob(this.name)
    }
}

val bobCopy : Bob = bob.copy() as Bob //Now a cast is required

So although we could have Bob.copy() return Cook, we now have to immediately downcast the result back into a Bob object. This doesn’t really make a lot of sense for a copy() method, which is why it’s acceptable to override a method by returning a child class type when needed.

Putting it Together

Here is an example program that shows off covariant return types.

package ch1.overriding

abstract class Cook(val name : String) {

    //Abstract method that returns Cook
    abstract fun copy() : Cook
}

class Bob(name : String) : Cook(name) {

    /**
     * Notice how this method changes the
     * return type from Cook to Bob. This is legal
     * because Bob is a child class of Cook.
     *
     * This is called covariant return types
     */
    override fun copy(): Bob {
        return Bob(this.name)
    }
}

fun main(args : Array<String>){
    val bob = Bob("Bob")

    //No casting required since copy() returns a Bob
    val bobCopy : Bob = bob.copy()
    val cookCopy : Cook = bob.copy()

    println(bob.name)
    println(bobCopy.name)
    println(cookCopy.name)
}

Here is the output when run

Bob
Bob
Bob
%d bloggers like this: