Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overriding & Dynamic Binding

Overriding Methods

A method inherited from an open superclass can be overridden (i.e., replaced) in a subclass. For this to happen, you need to

  • Declare the method in the superclass as open for overriding
  • Use the override keyword when defining the method in the subclass

Try this out now:

  1. Edit the file Override.kt, in the tasks/task15_3 subdirectory of your repository. Add to this file the following class definitions:

    open class Person {
        fun speak() {
            println("I am a Person")
        }
    }
    
    class Student(val degree: String) : Person() {
    }
    
  2. Try compiling the code. This should succeed without any problems.

  3. Now add the following method definition to Student:

    fun speak() {
        println("I am a Student, studying $degree")
    }
    
  4. Try recompiling the code. What error do you see?

  5. Follow the advice of the error message, and add an override modifier in Student. Try recompiling the code. What error do you see now?

  6. Fix the error by adding the open modifier to the definition of speak() in the Person class. You should find that the code now compiles successfully.

Overriding Properties

It is also possible to override properties inherited from a superclass. As with methods, the property in the superclass must declared as open for overriding, and the redefinition of the property in the subclass must be made using the override modifier.

open class Shape {
    open val isPolygon = false
}

class Triangle : Shape() {
    override val isPolygon = true
}

Caution

Note that when you override a property, the redefinition of the property in the subclass must be of the same type, or a subtype. So you are not allowed to override a Boolean property with an Int, for example.

Also, you can override a val property with a var, but you cannot override a var property with a val. This is because a var property has both a getter and a setter, whereas a val has only a getter. If we allowed a var to be overridden by a val, this would effectively remove functionality in the subclass, violating the Liskov substitution principle.

What Happens Here?

Let’s return to the Person and Student classes seen earlier, and consider a small program that uses these classes:

open class Person {
    open fun speak() {
        println("I am a Person")
    }
}

class Student(val degree: String) : Person() {
    override fun speak() {
        println("I am a Student, studying $degree")
    }
}

fun main() {
    val p = Person()
    p.speak()

    val s = Student("Computer Science")
    s.speak()

    val p2: Person = Student("Maths")
    p2.speak()
}

The main program here contains three examples of creating an object and invoking its speak() method. But what do we see printed on the console in each case?

Try the following quiz before adding this code to your Task 15.3 solution and testing it.

Dynamic Binding

When an open method from a superclass is overridden in subclasses, this allows dynamic binding (or ‘run-time binding’) of method calls to take place. In dynamic binding, a determination of which method to call is made at run time.

The counterpart to dynamic binding is static binding (or ‘compile-time binding’). In static binding, a determination of which method to call can be made at compile time.

Methods are open for overriding by default in Java, so dynamic binding is always possible, unless we prevent overriding by marking a method as final. In Kotlin, it is the other way around; dynamic binding cannot happen unless we mark a method as open and then override it in subclasses. C++ is a bit like Kotlin in this regard, in that static binding is the default and special steps are needed to enable dynamic binding.

Object-oriented languages typically implement dynamic binding by providing a method dispatch table (also known as a ‘vtable’) for each class.

Consider the following Kotlin classes:

open class X {
    fun method1() {}
    open fun method2() {}
    override fun toString() = "hello"
}

class Y : X() {
    override fun method2() {}
    fun method3() {}
}

Because Kotlin classes inherit implicitly from Any, the full class hierarchy looks like this:

classDiagram
direction LR
Any <|-- X
X <|-- Y
class Any {
    equals(other: Any?) Boolean
    hashCode() Int
    toString() String
}
class X {
    method1()
    method2()
    toString() String
}
class Y {
    method2()
    method3()
}

The method dispatch table for the class Y could therefore look something like this:

Method nameCode invoked
equalsAny.equals()
hashCodeAny.hashCode()
method1X.method1()
method2Y.method2()
method3Y.method3()
toStringX.toString()

Now imagine we have a variable thing, defined like this:

val thing: X = Y()

To resolve the method call thing.method2(), we look at the object referenced by thing, see that it is of type Y, then look for method2 in the dispatch table of Y. The dispatch table tells us that the version of method2 defined in class Y is the one that we need to call.

Now imagine that the method call is thing.toString(). Resolving this via the same process will lead to invocation of the version of toString() defined in X. Similarly, thing.hashCode() will resolve to the version of hashCode() defined in Any.