Avoid this bug in Kotlin

Trevor Hackman
4 min readNov 13, 2021

--

Do you see the bug with the following code? There’s a big one.

val task = {
doSomething()
start()
}

fun start() {
handler.postDelayed(task, 1000)
}
fun stop() {
handler.removeCallbacks(task)
}

If you’re unfamiliar with Handler, it’s an old but often used Android class for simple cases of timing and multithreading that takes Runnable (Hint).

This code intends to allow for a repeating task to be started and stopped. The problem is that it will never stop. stop() is totally broken.

The problem is not specific to Android or to Handler, but specific to Kotlin high-order functions and functional interfaces. You see task is of type () -> Unit but handler takes type Runnable. Kotlin is a language full of sugar, usually great sugar, but basically Kotlin allows you to pass () -> Unit even though Runnable is required because it’s a functional interface and they have matching signatures, i.e. they both take zero parameters and no return. But behind the scenes there is an implicit conversion. And to be clear these two types have no relationship and cannot cast to each other.

When digging deep into Kotlin, you can see what’s really going on by looking at the generated bytecode. But if that’s too alien for you, then you can look at it in Java from the decompiled bytecode. Normally far messier, but I have simplified what’s happening below for stop() in Java.

public final void stop() {
Runnable newRunnable = new Runnable() {
public final void run() {
task.invoke();
}
};

handler.removeCallbacks(newRunnable);
}

A new Runnable will be created each and every time. handler.removeCallbacks(task) will then fail to do anything because every time it’s being given a new Runnable and not a reference to task as it appears in the extremely misleading Kotlin code. Different code might not fail from this, but there is at the very least some unexpected object creation going on which may be bad for performance.

The Fix

The fix is very simple. Just one word is needed.

val task = Runnable {
doSomething()
start()
}

The Kotlin functional interfaces document refers to this as SAM conversion. It’s a short way to create an object of a functional interface. Now task is type Runnable so there won’t be any hidden function-to-functional-interface conversion or object creation and we don’t have anything to worry about!

Java vs Kotlin Syntax

A functional interface is an interface with a single abstract method (SAM). Note though, that in Java any SAM interface will be treated as a functional interface. But in Kotlin a functional interface must also use the fun keyword.

// This is a functional interface
fun interface KotlinRunnable {
fun run()
}

// This has a single abstract method,
// But is not a functional interface
interface KotlinNonFunRunnable {
fun run()
}

Java doesn’t have the fun keyword or any keyword for functions, but introduced the concept of functional interfaces in Java 1.8 with an informative annotation.

@FunctionalInterface
public interface Runnable {
public abstract void run();
}

But note, @FunctionalInterface is merely informative, not required. All interfaces written in Java with only a single abstract method will be treated as functional interfaces by the Java and Kotlin compilers regardless of whether it has the annotation or not.

Java vs Kotlin Behavior

Okay, here’s the really weird part. Kotlin’s hidden function-to-functional-interface conversion works differently depending on if the functional interface was written in Java or in Kotlin.

val task = {}

val kotlinFunInterfaceSet = mutableSetOf<KotlinRunnable>()
val javaFunInterfaceSet = mutableSetOf<Runnable>()

fun main() {
kotlinFunInterfaceSet.add(task)
kotlinFunInterfaceSet.add(task)

javaFunInterfaceSet.add(task)
javaFunInterfaceSet.add(task)

println("Kotlin tasks = ${kotlinFunInterfaceSet.size}")
println("Java tasks = ${javaFunInterfaceSet.size}")
}

We have two sets, one for Java’s Runnable and one for KotlinRunnable which mirrors Java’s Runnable but is written in Kotlin. Sets don’t allow duplicates, so if the same task were passed then the sets should both be of size one after two adds, but as was revealed above, new objects will be created by the conversion. However this is the printed result,

Kotlin tasks = 1
Java tasks = 2

If you look at the decompiled bytecode, a new object is still getting created every time a conversion to a Kotlin functional interface happens. The difference I discovered is in logical equality or the double equals evaluation.

val task = {}

val kotlinList = mutableListOf<KotlinRunnable>()
val javaList = mutableListOf<Runnable>()

fun main() {
kotlinList.add(task)
kotlinList.add(task)

javaList.add(task)
javaList.add(task)

val kotlinDoubleEquals = kotlinList[0] == kotlinList[1]
val kotlinTripleEquals = kotlinList[0] === kotlinList[1]
val javaDoubleEquals = javaList[0] == javaList[1]
val javaTripleEquals = javaList[0] === javaList[1]

println("Kotlin $kotlinDoubleEquals $kotlinTripleEquals")
println("Java $javaDoubleEquals $javaTripleEquals")
}

Kotlin true false
Java false false

Referential equality or triple equals is false for both Runnable and KotlinRunnable, which shows new objects are getting created for both, but logical equality or double equals is different. Instances of Kotlin functional interfaces that are created by function-to-functional-interface conversion from the same function have logical equality. But that’s not the case for Java functional interfaces. In conclusion, the behavior is very different depending on if your functional interface was written in Java or Kotlin. Not a good sight.

Done on Kotlin 1.5.31

--

--

Trevor Hackman
Trevor Hackman

Written by Trevor Hackman

Hi, I am an Android developer.

Responses (1)