Kotlin Method Resolution
When you work with inheritance in Kotlin, understanding how the language determines which method to call is crucial. This concept, known as method resolution, affects how your objects behave at runtime and is a fundamental aspect of object-oriented programming.
Introduction to Method Resolution
Method resolution (also called "dispatch") is the process by which Kotlin determines which implementation of a method to call when you invoke a method on an object. In inheritance hierarchies where classes override methods from their parent classes, Kotlin needs clear rules to decide which version of the method to execute.
Let's explore how Kotlin resolves methods and the rules that govern this process.
Basic Method Resolution Rules
1. Static vs. Dynamic Dispatch
Kotlin uses two types of method dispatch:
- Static dispatch: The method to call is determined at compile time
- Dynamic dispatch: The method to call is determined at runtime based on the actual type of the object
open class Animal {
open fun makeSound() {
println("Generic animal sound")
}
fun eat() {
println("Animal is eating")
}
}
class Dog : Animal() {
override fun makeSound() {
println("Woof!")
}
fun fetch() {
println("Dog is fetching")
}
}
fun main() {
val animal: Animal = Dog()
animal.makeSound() // Dynamic dispatch - calls Dog's implementation
animal.eat() // Static dispatch - calls Animal's implementation
// animal.fetch() // Won't compile - fetch() isn't visible through Animal reference
}
Output:
Woof!
Animal is eating
In this example:
makeSound()
uses dynamic dispatch because it's marked asopen
and overriddeneat()
uses static dispatch because it isn't overriddenfetch()
isn't accessible through anAnimal
reference
Overriding Rules and Method Resolution
Method Overriding Basics
For Kotlin to correctly resolve methods, it follows specific rules for method overriding:
- Methods in the parent class must be marked with
open
to allow overriding - Overriding methods in child classes must use the
override
keyword - The method signature (name, parameters, and return type) must be compatible
open class Shape {
open fun draw() {
println("Drawing a shape")
}
open fun getArea(): Double {
return 0.0
}
}
class Circle(private val radius: Double) : Shape() {
override fun draw() {
println("Drawing a circle with radius $radius")
}
override fun getArea(): Double {
return Math.PI * radius * radius
}
}
class Rectangle(private val width: Double, private val height: Double) : Shape() {
override fun draw() {
println("Drawing a rectangle with width $width and height $height")
}
override fun getArea(): Double {
return width * height
}
}
fun main() {
val shapes = listOf(
Shape(),
Circle(5.0),
Rectangle(4.0, 3.0)
)
for (shape in shapes) {
shape.draw()
println("Area: ${shape.getArea()}")
println("---")
}
}
Output:
Drawing a shape
Area: 0.0
---
Drawing a circle with radius 5.0
Area: 78.53981633974483
---
Drawing a rectangle with width 4.0 and height 3.0
Area: 12.0
---
Here, the method resolution happens dynamically at runtime based on the actual object type.
Accessing Superclass Methods with super
Sometimes, you may want to call the parent class's implementation of a method from the child class. Kotlin provides the super
keyword for this purpose:
open class Vehicle {
open fun startEngine() {
println("Engine started")
}
}
class ElectricCar : Vehicle() {
override fun startEngine() {
super.startEngine() // Calls Vehicle's implementation first
println("Electric motor initialized")
}
}
fun main() {
val car = ElectricCar()
car.startEngine()
}
Output:
Engine started
Electric motor initialized
Method Resolution with Multiple Interfaces
When a class implements multiple interfaces that contain methods with the same signature, Kotlin requires explicit resolution using the super keyword with the interface name:
interface Flyable {
fun fly() {
println("Flying like a generic flyable object")
}
}
interface Bird {
fun fly() {
println("Flying like a bird")
}
}
class Parrot : Flyable, Bird {
override fun fly() {
// Must explicitly choose which interface's method to call
super<Flyable>.fly()
super<Bird>.fly()
println("Flying like a parrot")
}
}
fun main() {
val parrot = Parrot()
parrot.fly()
}
Output:
Flying like a generic flyable object
Flying like a bird
Flying like a parrot
Extension Functions and Method Resolution
Kotlin's extension functions add an interesting dimension to method resolution. Extension functions are resolved statically based on the declared type of the variable, not the runtime type:
open class Machine
class Computer : Machine()
fun Machine.getType() = "Generic machine"
fun Computer.getType() = "Computer"
fun main() {
val computer: Computer = Computer()
println(computer.getType()) // Calls Computer's extension
val machine: Machine = Computer()
println(machine.getType()) // Calls Machine's extension, not Computer's!
}
Output:
Computer
Generic machine
This behavior is different from regular method overriding, where the runtime type determines which method is called.
Real-World Application: Building a UI Component System
Let's see method resolution in action with a practical example of a UI component system:
open class UIComponent(val id: String) {
open fun render() {
println("Rendering basic component $id")
}
open fun handleClick() {
println("Click detected on $id")
}
}
open class Button(id: String, private val label: String) : UIComponent(id) {
override fun render() {
super.render()
println("Rendering button with label: $label")
}
override fun handleClick() {
super.handleClick()
println("Button $label was clicked")
}
}
class ImageButton(id: String, label: String, private val imagePath: String) : Button(id, label) {
override fun render() {
super.render()
println("Adding image from: $imagePath")
}
}
fun renderUI(components: List<UIComponent>) {
for (component in components) {
// Method resolution happens here
component.render()
println("---")
}
}
fun main() {
val components = listOf(
UIComponent("generic-1"),
Button("btn-1", "Submit"),
ImageButton("img-btn-1", "Profile", "/images/profile.png")
)
renderUI(components)
// Simulate clicking on the ImageButton
val imageButton = components[2]
imageButton.handleClick()
}
Output:
Rendering basic component generic-1
---
Rendering basic component btn-1
Rendering button with label: Submit
---
Rendering basic component img-btn-1
Rendering button with label: Profile
Adding image from: /images/profile.png
---
Click detected on img-btn-1
Button Profile was clicked
This example demonstrates how method resolution works in a real-world scenario where UI components inherit from each other and override rendering behavior.
Method Resolution with Companion Objects and Static Methods
Kotlin doesn't have static methods in the same way as Java, but companion objects provide similar functionality. Method resolution for companion objects follows different rules:
open class DatabaseConnection {
companion object {
fun create(): DatabaseConnection {
println("Creating base connection")
return DatabaseConnection()
}
}
}
class PostgresConnection : DatabaseConnection() {
companion object {
fun create(): PostgresConnection {
println("Creating Postgres connection")
return PostgresConnection()
}
}
}
fun main() {
// These are different methods, not overrides
DatabaseConnection.create()
PostgresConnection.create()
// This doesn't call PostgresConnection.create()
val connection: DatabaseConnection = PostgresConnection.create()
}
Output:
Creating base connection
Creating Postgres connection
Creating Postgres connection
Summary
Method resolution in Kotlin determines which implementation of a method to call when dealing with inheritance hierarchies. Key points to remember:
- Kotlin uses dynamic dispatch for
open
methods that areoverride
-n in subclasses - Static dispatch is used for non-overridden methods
- The
super
keyword allows access to parent class implementations - When implementing multiple interfaces with the same method, use
super<Interface>
for explicit resolution - Extension functions are resolved statically based on the declared type
- Companion object methods follow different resolution rules than instance methods
Understanding method resolution is crucial for predicting how your code will behave and for debugging complex inheritance hierarchies.
Exercises
- Create a
Vehicle
hierarchy with different implementations of adrive()
method and observe how method resolution works - Implement a class that extends from two interfaces with conflicting default implementations
- Create an extension function scenario that demonstrates the difference between extension function resolution and regular method resolution
- Design a logging system that uses method resolution to provide different logging implementations
Additional Resources
- Kotlin Official Documentation on Inheritance
- Kotlin Language Specification on Overriding
- Type-safe builders in Kotlin - An advanced application of method resolution
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)