Blog

7 common mistakes in Kotlin

10 Mar, 2023
Xebia Background Header Wave

This article was originally published at my personal blog on March 9, 2023.

Have you ever felt like this kid when programming? I sure did! When I got into Kotlin development I felt like this kid many many times. And even now I have some experience, every once in a while, when conducting a Kotlin training or when on a Kotlin assignment, I encounter situations where I simply cannot explain why the code doesn’t work the way I intended. Or maybe even worse: I can’t explain why the code DOES work. In this post I’ll show you 7 code snippets containing common mistakes (I made) when programming in Kotlin.

Hello World!

Let’s start off with a rather simple “hello world” example. Consider the code below. What do you expect the outcome of this program to be?

fun main(args: Array<String>) {
    fun hello() = print("Hello")

    fun world() = {
        print("World")
    }

    hello()
    world()
}

It might surprise you, but the output of this program will just be Hello.

This one is especially confusing and a common mistake for Scala developers moving to Kotlin. I have been doing Scala for years and with Scala both functions would be equivalent. In Kotlin, however, using the combination of = and {} means your function is returning a lambda and NOT executing it.

To make this example program print Hello World you would have to either remove the = or the {} in the function world , e.g. like this:

fun main(args: Array<String>) {
    fun hello() = print("Hello")

    fun world() {
        print("World")
    }

    hello()
    world()
}

Good luck, Scala developers!

Fallback

The next one is about a nifty little (extension) function, withDefault, that is available on Kotlin Map instances. This function allows you to define a default value to be returned when a missing key is retrieved. In the example below I create an empty Map and try to make sure it returns the phrase "default" for missing keys. Will this succeed?

fun main(args: Array<String>) {
    val map = mapOf<String, Int>().withDefault{ "default" }

    println(map["1"])
}

Unfortunately, the answer is no. This program will print null, which is probably not what you would expect here. The default value is ONLY considered when using the getValue function to access the map. If you use the indexing operator, the square brackets, under the hood the get function is used and this always returns null when the key is missing.

Let’s rewrite this using getValue.

fun main(args: Array<String>) {
    val map = mapOf<String, Int>().withDefault{ "default" }

    println(map.getValue("1"))
}

The program above will in fact print default. So, the solution is to always retrieve values from a Map with getValue when using withDefault? Not quite. Consider the following snippet:

fun main(args: Array<String>) {
    val map = mapOf<String, Int>().withDefault{ "default" }

    println(map.filterKeys { it != "2" }.getValue("1"))
}

This code will actually throw a java.util.NoSuchElementException!

Exception in thread "main" java.util.NoSuchElementException: Key 1 is missing in the map.
 at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24)
 at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:348)
 at DefaultKt.main(Default.kt:4) 

Why is that? Using any transforming operations on Kotlin read-only collections (more on that in the next section) will in fact return a new instance of that collection type. When we use filterKeys on our Map instance, we get a new instance of Map. An instance without the default defined. Hence, getValue still won’t have a default value to fallback to and throws an exception.

To summarise, my advice is to avoid using withDefault all together. It’s not reliable. Instead, always specify the fallback value explicitly when retrieving from a Map by using the getOrDefault function. For example:

fun main(args: Array<String>) {
    val map = mapOf<String, Int>()
    println(map.filterKeys { it != "2" }.getOrDefault("1", "default"))
}

This program will actually output default as we intended (because the key "2" is missing). Don’t use withDefault, use getOrDefault when you need to rely on a fallback value.

Immutability

As already mentioned above, Kotlin has read-only collections. If you use any of the <collection type>Of() functions (e.g. listOf() or mapOf()) you’ll get a read-only instance of that collection type. Using any of the mutable<collection type>Of() functions will create a mutable collection of that type.

It is a common mistake in Kotlin to confuse read-only collections with immutable collections. Read-only collections are similar to immutable collections in that they both provide collections that cannot be modified once they are created. However, there are some differences between the two concepts. Read-only collections are collections that can still be modified by their underlying data source, but cannot be modified directly through the reference to the collection. For example, if a read-only list is backed by an ArrayList, the contents of the ArrayList can still be modified, but the reference to the read-only list cannot be used to add, remove, or modify elements.

Immutable collections (e.g. in Scala) are truly immutable and cannot be modified by any means. Once an immutable collection is created, its contents cannot be changed, and any attempt to do so will result in a new immutable collection being created. This means that an immutable collection can provide stronger guarantees of safety and immutability than a read-only collection in Kotlin.

I’ll demonstrate this using the code below

import java.util.*

fun main(args: Array<String>) {
    val readonly = listOf(1, 2, 3)

    if(readonly is MutableList) {
      readonly.add(4)
    }

    println(readonly)
}

You might expect this code would not even compile, since the add operation is not available on read-only collections and we just learned that listOf would create a read-only list. The List interface does not define an add function. Sadly, this code actually compiles and running it will throw an UnsupportedOperationException.

Exception in thread "main" java.lang.UnsupportedOperationException
 at java.base/java.util.AbstractList.add(AbstractList.java:153)
 at java.base/java.util.AbstractList.add(AbstractList.java:111)
 at ImmutabilityKt.main(Immutability.kt:6)

Why does this code even compile? Kotlin’s smart casting mechanism is in play here. The compiler tracks the is-checks and inserts (safe) casts automatically. As a result it changes the type of the readonly variable to MutableList. Unlike Kotlin’s List interface, theMutableList interface has the add operation available.

But why would the is MutableList check even pass? This is where the true difference between read-only collections and immutable collections becomes apparent. While the listOf function returns a read-only list of type List, the concrete type is the plain oldArrayList (java.util.Arrays$ArrayList). This collection is mutable by design. So, our read-only list is actually backed by a mutable implementation! Read-only collections only protect you on the interface level. Under the hood standard mutable Java collections are used, so you can still mess things up if you are not careful.

Knowing this, can we break things even more? Sure we can! Consider the following code

fun main(args: Array<String>) {
    val readonly = listOf(1, 2, 3)

    Collections.sort(readonly, reverseOrder())

    println(readonly)
}

It might surprise you (but shouldn’t anymore after the discovery we made above) that this program actually outputs [3, 2, 1]. We managed to modify our read-only list (!) by passing it to a sort function.

I shot myself in the foot by using mutable data structures way too many times. Be extra careful when using read-only collections in Kotlin. They only protect you to a certain extent.

Equality

The next example involves Kotlin data classes, marked with the data keyword. These are classes whose main purpose is to hold data and of which standard functionality can be derived from the data by the compiler. Among those automatically derived functions are the equals() and hashCode() functions. Those functions are automatically generated by the compiler based on the properties of the class. They are used to determine equality and collision, for instance when adding elements to a set.

Let’s have a look at the code below. We are creating two instances of data class A, a and b, with the same value for the num property. We change the value of the name property of b to "b". Then we add a and b to a Set in order to keep only unique instances of data class A.

fun main(args: Array<String>) {
    data class A(val num: Int) {
        var name: String = "a"
    }
    val a = A(1)
    val b = A(1)
    b.name = "b"

    val unique = setOf(a, b)
    println(unique)
}

You might think that a and b shouldn’t be considered equal, since they have a different value for the name property, and thus should be both added to the unique Set. However, the compiler only takes properties into account that are part of the constructor of a data class. So in this case it only takes num into account.

I never encountered this one myself, but it is implicit and obscure enough to be considered as a pitfall in Kotlin, if not a common mistake.

Type Safety

The next pitfall I discovered during a basic Kotlin training that I was conducting. My goal was to demonstrate strong typing and generics to my students. In a REPL, I showed them the following example code.

fun main(args: Array<String>) {
    class Container<T>(val value: T)

    val noValue: Any? = null
    val container = Container(noValue)
    println(container.value is Int?)
}

It was quite a shock to find out this program actually outputs true. But why? If I put in Any?, this becomes a Container<Any?>. The property value will also be of type Any?. So, the type check container.value is Int? should return false.

The reason of this behaviour becomes clear after decompiling the byte code as shown in the video below.

Unexpected behaviour regarding type safety

As you can see in the video, the resulting Java code from the decompiled byte code provides the answer to this mystery. Apparently the type check always returns true if the value is null. D’oh!

Zip it!

In the program below we are trying to zip two lists, a list of keys and a list of values, and create a map of the resulting list of tuples. The zip function returns a list of tuples with the type List<Pair<Int, String>>. The mapOf function accepts tuples as a vararg argument: vararg pairs: Pair<K, V>. We use the spread operator * on zipped to be able to pass it as a vararg argument.

fun main(args: Array<String>) {
    val keys = listOf(1, 2, 3)
    val values = listOf("a", "b", "c")
    val zipped = keys.zip(values)

    // * is the spread operator, that unpacks the array into a list of values
    println(mapOf(*zipped))
}

Given the type of zipped and the signature of mapOf we might expect this program to print {1=a, 2=b, 3=c}. Unfortunately, it results in a rather cryptic compilation error.

None of the following functions can be called with the arguments supplied.

mapOf(vararg Pair<TypeVariable(K), TypeVariable(V)>)   
  where K = TypeVariable(K), V = TypeVariable(V) for    
  fun <K, V> mapOf(vararg pairs: Pair<K, V>): Map<K, V> defined in kotlin.collections
mapOf(Pair<TypeVariable(K), TypeVariable(V)>)   
  where K = TypeVariable(K), V = TypeVariable(V) for    
  fun <K, V> mapOf(pair: Pair<K, V>): Map<K, V> defined in kotlin.collections

This can be explained by type erasure. Due to type erasure some of the type information is only available at compile time and not at runtime. It’s a common mistake in Java or Kotlin to forget about type erasure and this is something to get easily bitten by when dealing with parameterised types.

In the example code above the type of zipped is List<Pair<Int, String>>. However the type arguments Int and String are only known at compile time. At runtime all we know is that this is a Pair. Using the spread operator * we pass Pair values as vararg arguments. We no longer have the type information available of the inner types, hence the compilation error.

We can fix this program by applying a little tweak. To preserve type information when passing arguments to a varargs function, you’ll need to use the toTypedArray function.

fun main(args: Array<String>) {
    val keys = listOf(1, 2, 3)
    val values = listOf("a", "b", "c")
    val zipped: List<Pair<Int, String>> = keys.zip(values)

    // * is the spread operator, that unpacks the array into a list of values
    println(mapOf(*zipped.toTypedArray()))
}

Our modified program will output{1=a, 2=b, 3=c} as intended.

Extensions

The last Kotlin gotcha involves extension functions and type hierarchy. Consider the following program. We have two classes, A and B, where B inherits from A. We define an extension function extensionFun for each of them that outputs "A" respectively "B". Then we define a third function, extensionFun, that takes one argument, myVar, of type A. This function calls the corresponding extension function of myVar and prints the result to the console. What is the output?

fun main(args: Array<String>) {
    open class A
    class B : A()

    fun A.extensionFun() = "A"
    fun B.extensionFun() = "B"

    fun extensionFun(myVar: A) {
        println(myVar.extensionFun())
    }
    extensionFun(B())
}

The output of this program is "A". At compile time it is determined which extension function should be called. It doesn’t matter what the runtime type is. In this case the compiler sees that myVar is of type A, so version A of extensionFun is called.

Extension functions are a really powerful tool. But you should be really careful when “overloading” extension functions in a type hierarchy.

Conclusion

That’s all for now! I’ve shown 7 common mistakes in Kotlin of which most of them I ran into myself. To summarise the “features” to be aware of:

  • Using = and {} in your function definition means your function is returning a lambda and NOT executing it. I’m looking at you, Scala developers 😉
  • Avoid using withDefault. Instead, always specify the fallback value explicitly when retrieving from a Map by using the getOrDefault function.
  • Read-only != immutable. Be aware of this when working with read-only collections and especially be careful when passing read-only collections to Java API’s. Here’s another great post highlighting the differences between read-only and immutable, in this case with regards to the val keyword.
  • Auto generated equality checks only take properties into account that are part of the constructor of a data class
  • Type comparisons on nullable types can be tricky! For null values the types are considered equal.
  • Type erasure can get in your way when using the spread operator (*). Use toTypedArray to preserve type information.
  • Runtime polymorphism doesn’t mix with extension functions. Which extension function to call is determined at compile time.
Jeroen Rosenberg
Dev of the Ops / foodie / speaker / blogger. Founder of @amsscala. Passionate about Agile, Continuous Delivery. Proud father of three.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts