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.