Kotlin Polymorphism

Polymorphism allows computer code to become contextual. In other words, a computer instruction can take on different meanings depending on situation in which the instruction is used. This is no different than how we speak. A person can use the same word, ‘there’, ‘they’re’, or ‘their’ to mean different things even though all three words are said the same way.

Kotlin supports two forms of polymorphism because it is both strongly and statically typed. The first form of polymorphism happens when the code is compiled. The other form happens at runtime. Understanding both forms of polymorphism is critical when writing code in Kotlin.

Compile Time Polymorphism

Let’s consider the following code snippet.

fun printNumber(n : Number){
    println("Using printNumber(n : Number)")
    println(n.toString() + "\n")
}

fun printNumber(n : Int){
    println("Using printNumber(n : Int)")
    println(n.toString() + "\n")
}

fun printNumber(n : Double){
    println("Using printNumber(n : Double)")
    println(n.toString() + "\n")
}

We have three functions all of which have the same name and return type (void). As a matter of fact, the only difference in the signature of these methods is the type of parameter that is used. The first printNumber accepts a Number variable. The second one accepts an Int variable, and the final one accepts a Double variable. This technique is called function overloading.

Since all functions have the same name, how do we know which one will be used when we have code such as the snippet below?

val a : Number = 99
val b = 1
val c = 3.1

printNumber(a) //Which version of printNumber is getting used?
printNumber(b) //Which version of printNumber is getting used?
printNumber(c) //Which version of printNumber is getting used?

The compiler sees three functions with the same name and knows it ultimately has to choose a version of printNumber to use on each statement. Since all three functions have the same name, the compiler turns to context clues to deduce which printNumber function should be used in each case. The variable ‘a’ is explicity declared as type Number. Since there is a version of printNumber that accepts a Number parameter, that version of printNumber is chosen.

The next variable, b, is assigned to one. The number one is an integer, and since we didn’t tell the compiler to make it a Number, it defaults to an Integer type. When printNumber(b) is called, the compiler matches to the version of printNumber that accepts type Integer. The final variable, ‘c’, is initialized to 3.1. The default type for that number is Double so the compiler chooses printNumber(Double) as the correct version to call when ‘c’ is passed as an argument.

It should be noted that when the compiler can’t correctly pick a method, it will issue an error. We are free to overload functions as long as the method signatures are unique enough for the compiler to figure out which function should be used. So for example, functions may have the same return type provided that they take different parameters or a different number of parameters. What isn’t allowed is for two functions to have the same parameters but different return types. In that case, the compiler is unable to figure out which function to use and it will issue an error.

Function overloading is a powerful technique that allows us to write flexible code. The main use case is to write functions that have different behaviors based on the type of object. Imagine writing a function that converts objects to JSON (a form of data exchange). Since every object can be different, we would need different implementations of a functions to correctly output JSON for each type of object. However, it doesn’t really make sense to write things like intToJson, arrayToJson, or carToJson. We can call of these functions by the same name: toJson, and just use polymorphism to call the proper implementation.

Runtime Polymorphism

Runtime polymorphism happens when the program is running. Since Kotlin is an OOP language, we can use classes and interfaces to refer to objects. For example, Number is a class while Integer and Double are both child classes of Number. That it is acceptable to use Integer or Double variables anywhere in our code where Number is expected.

However, things go even futher than using child classes as values for a base class. When a child class overrides a method defined in a base class, the program will use the child class’s method rather than the base class. This is called virtual methods. Let’s consider Number’s toDouble() method.

The toDouble() method is defined in Number, which means all child classes of toDouble() have some sort of an implementation of toDouble(). When we call toDouble() on an Integer, the program knows to use the toDouble() defined in Integer so that the output of toDouble() makes sense for an Integer. Likewise where we to call toDouble() on a BigDecimal object, we would also get the version of that method defined in BigDecimal.

In our code example, let’s consider summing all of our number variables.

fun sum(numbers : List<Number>) : Number {
    return numbers.sumByDouble { it.toDouble() }
}

fun main(args : Array<String>){
    val a : Number = 99
    val b = 1
    val c = 3.1

    //Using runtime polymorphism
    println("Summing all numbers")
    println(sum(listOf(a, b, c)))
}

Since the varaibles ‘a’, ‘b’, and ‘c’ are all either types of Number or child classes of Number, we can use them in the sum function. Inside of the function, we call it.toDouble() to convert the current number into a double. One of those numbers is the variable ‘b’ which is an Integer. So when b.toDouble() gets called, the version associated with Integer is used. However, when c.toDouble() gets called, the version of toDouble() associated with Double gets used.

Putting it together

Both forms of polymorphism allow for highly flexible code. Well designed computer code should be written with generalization in mind. By targeting a base class or an interface, we can reuse the same code with different types of objects by using polymorphism. Likewise, function overloading improves the readability of code and it’s maintainability because we can call a function with the same name as other functions and trust that the proper function is used depending on the context.

Example Program

/**
 * The first 3 functions, all called printNumber, demonstrate function
 * overloading. This is polymorphism that is determined at compile time.
 * Basically, the compiler knows which function to use based on the type of the
 * input parameter n. So if n is an Int, it will use printNumber(n : Int).
 * If n is a double, it will use printNumber(n : Double). For all other numbers,
 * it will use printNumber(n : Number).
 */
fun printNumber(n : Number){
    println("Using printNumber(n : Number)")
    println(n.toString() + "\n")
}

fun printNumber(n : Int){
    println("Using printNumber(n : Int)")
    println(n.toString() + "\n")
}

fun printNumber(n : Double){
    println("Using printNumber(n : Double)")
    println(n.toString() + "\n")
}

/**
 * This function shows runtime polymorphism. In this case, all objects are of type Number.
 * Number has a toDouble() method which is different for each kind of number. However, since
 * all classes that extend Number must implement toDouble(), we can trust that longs, ints, floats,
 * etc can all make the conversion to a double when needed.
 */
fun sum(numbers : List<Number>) : Number {
    return numbers.sumByDouble { it.toDouble() }
}

fun main(args : Array<String>){
    val a : Number = 99
    val b = 1
    val c = 3.1

    //Using compile time polymorphism
    printNumber(a)
    printNumber(b)
    printNumber(c)

    //Using runtime polymorphism
    println("Summing all numbers")
    println(sum(listOf(a, b, c)))
}
Advertisement

One thought on “Kotlin Polymorphism”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: