1. Overview

Lambda expressions allow us to express behaviors in a more concise and elegant fashion in Kotlin.

In this tutorial, we’re going to see how Kotlin lambda expressions are interoperable with Java functional interfaces using SAM conversions. Along the way, we’ll dig deeper to see the internal representation of this interoperability at the bytecode level.

2. Functional Interfaces

In Java, lambda expressions are implemented in terms of functional interfaces. Functional interfaces have only one abstract method to implement.

With Kotlin, on the other hand, we have proper function types at compile-time. For instance (String) -> Int is a function accepting a String as the input and returning an Int as the output.

Despite their implementation differences, Kotlin lambdas are fully interoperable with functional interfaces in Java.

To make matters more concrete, let’s consider the java.lang.Thread class. This class has a constructor that takes a Runnable interface instance as the input:

public Thread(Runnable target) {
    // omitted

Before Java 8, we had to create an anonymous inner class from Runnable to create a Thread instance:

Thread thread = new Thread(new Runnable() {
    public void run() {
        // the logic

Here we’re creating both a subclass and an instance at the same time.

In Kotlin, similarly, we can use object expressions to achieve the same thing:

val thread = Thread(object : Runnable {
    override fun run() {
        // the logic

However, that doesn’t feel quite right and is far from being elegant and concise.

The good news is because lambda expressions in Kotlin are so interoperable with functional interfaces in Java, we can pass a lambda to the Thread constructor:

val thread = Thread({
    // the logic

This is obviously a much more pleasant API. Since the last parameter is a lambda, we can omit the parenthesis and move the lambda block out of it:

val thread = Thread {
    // the logic

Simply put, even though the Java API is expecting a functional interface from us, we’re still able to pass the corresponding lambda to it. Under the hood, the Kotlin compiler will convert the lambda to a functional interface.

Now that we know about this possibility, let’s dig deeper and see how things are working under the hood.

3. SAM Conversions

The lambda to functional interface conversion works because functional interfaces only have one abstract method. Such interfaces are called Single Abstract Method or SAM interfaces. Also, this automatic conversion is also known as SAM conversions.

Under the hood, however, Kotlin compiler still creates an anonymous inner class for SAM conversions. For instance, let’s consider the same Thread example:

val thread = Thread {
    // the logic

If we compile the code using kotlinc:

$ kotlinc SamConversions.kt

Then we can inspect the bytecode using javap:

$ javap -v -p -c SamConversionsKt
// truncated
0: new           #11       // class java/lang/Thread
3: dup
4: getstatic     #17       // Field SamConversionsKt$main$thread$1.INSTANCE:LSamConversionsKt$main$thread$1;
7: checkcast     #19       // class java/lang/Runnable
10: invokespecial #23      // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
13: astore_0
// truncated
  static final #13;        // class SamConversionsKt$main$thread$1

Here’s what the Kotlin compiler has generated for us:

  1. It defines an anonymous inner class — the SamConversionsKt$main$thread$1 part
  2. It gets the singleton instance of that class — the SamConversionsKt$main$thread$1.INSTANCE part
  3. Then, it makes sure it’s a Runnable instance, and after that
  4. It passes that instance to the Thread constructor

3.1. Object Expressions

As we mentioned earlier, we can achieve the same thing using object expressions:

Thread(object : Runnable {
    override fun run() {
        // the logic

However, as the bytecode shows below, this will create an anonymous inner class every time we create a Thread instance:

0: new           #11      // class java/lang/Thread
3: dup
4: new           #13      // class SamConversionsKt$main$thread$1
7: dup
8: invokespecial #16      // Method SamConversionsKt$main$thread$1."<init>":()V
11: checkcast     #18     // class java/lang/Runnable
14: invokespecial #21     // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
17: astore_0

At index 8, the JVM is creating an instance of the anonymous inner class and calling its instance initialization method, that is, it’s constructor.

On the other hand, the lambda will create a singleton instance and reuse that instance every time we need it:

4: getstatic #17 // Field SamConversionsKt$main$thread$1.INSTANCE:LSamConversionsKt$main$thread$1;

So, in addition to being more concise, the lambda expression and SAM conversion will create fewer objects and therefore will exhibit a better memory footprint.

3.2. Closures

This performance gain, however, can be fragile if we capture a variable from the outer scopes:

var answer = 42
val thread = Thread {

In that case, we see an important change:

23: invokespecial #25   // Method SamConversionsKt$main$thread$1."<init>":(Lkotlin/jvm/internal/Ref$IntRef;)V
26: checkcast     #27   // class java/lang/Runnable

As shown above, the anonymous inner class now accepts one argument in its constructor. As a matter of fact, the Kotlin compiler will wrap the captured variable inside an IntRef instance and passes it to the inner class constructor.

Therefore, if we capture a variable inside a lambda, the JVM will create one instance per invocation. So we’ll lose that performance gain. To avoid this, we can use inline functions in Kotlin.

4. SAM Constructors

Usually, the conversion of lambda expressions to the corresponding functional interfaces happens automatically by the compiler. However, sometimes we need to perform this conversion manually.

For instance, the ExecutorService interface provides two overloaded versions of submit():

Future<T> submit(Callable task);
Future<?> submit(Runnable task);

Both Callable and Runnable are functional interfaces. So if we write:

val result = executor.submit {
    return@submit 42

Then the Kotlin compiler can’t infer which overloaded version we’re using. To resolve this confusion, we can use SAM constructors:

val submit = executor.submit(Callable {
    return@Callable 42

As shown above, the name of a SAM constructor is the same as the name of the underlying functional interface. Also, the constructor itself is a special compiler-generated function that lets us perform explicit conversion of a lambda into an instance of a functional interface.

Moreover, SAM constructors are also useful when returning a functional interface:

fun doSomething(): Runnable = Runnable {
    // doing something

Or even storing a lambda to a variable:

val runnable = Runnable { 
    // doing something

Please note that SAM conversions only work for functional interfaces and not for abstract classes, even if those abstract classes have only one abstract method.

4.1. SAM Conversion for Kotlin Interfaces

As of Kotlin 1.4, we can use SAM conversions for Kotlin interfaces as well. All we have to do is to mark a Kotlin interface with the fun modifier:

fun interface Predicate<T> {
    fun accept(element: T): Boolean

Now we can apply SAM conversions to instances of this interface:

val isAnswer = Predicate<Int> { i -> i == 42 }

Please note that when we apply the fun modifier to a Kotlin interface, we should make sure that the interface contains exactly one single abstract method. Otherwise, the code won’t compile:

fun interface NotSam {
    // no abstract methods

The above code will fail at compilation with the error message:

Fun interfaces must have exactly one abstract method

5. Conclusion

In this article, we looked at SAM interfaces. We also saw how Kotlin will convert lambda expressions into functional interfaces in Java. This enabled us to pass lambda expressions wherever functional interfaces were expected.

Finally, we learned that sometimes we have to tell the compiler to make this conversion explicitly via SAM constructors.

As usual, all the examples are available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.