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
openfor overriding - Use the
overridekeyword when defining the method in the subclass
Try this out now:
-
Edit the file
Override.kt, in thetasks/task15_3subdirectory 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() { } -
Try compiling the code. This should succeed without any problems.
-
Now add the following method definition to
Student:fun speak() { println("I am a Student, studying $degree") } -
Try recompiling the code. What error do you see?
-
Follow the advice of the error message, and add an
overridemodifier inStudent. Try recompiling the code. What error do you see now? -
Fix the error by adding the
openmodifier to the definition ofspeak()in thePersonclass. 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
}
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 name | Code invoked |
|---|---|
equals | Any.equals() |
hashCode | Any.hashCode() |
method1 | X.method1() |
method2 | Y.method2() |
method3 | Y.method3() |
toString | X.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.