From Sam to Function when converting Java to Kotlin

Update: Kotlin 1.4 announced that SAM will be supported for Kotlin (https://youtrack.jetbrains.com/issue/KT-7770). The problem presented in this story is completely solved by JetBrains/Kotlin team.

SAM conversion is a subtle feature that sparks joy when converting the codebase from Java to Kotlin. However, it is annoying when it does not work as you expected. This documents the pleasure and learnings when my team is converting our codebase from Java to Kotlin.

Background

Functional interface, also known as SAM (Single Abstract Method), was introduced to Java 8. It can be combined with lambda expression and method reference in Java. Lambda expression simplifies Java code by more compactly allowing users to express instances of single-method classes. When a lambda expression does nothing but calling an existing method, method reference can be used to make the code even more concise.

In Kotlin, function is first-class. This indicates that functions can be stored in variables and data structures, passed as arguments to and returned from other higher-order functions. Because the function is regarded as a first-class citizen, Kotlin does not need to support converting functions into implementations of Kotlin interfaces for SAM. This is a conscious decision.

In other words, Kotlin supports SAM conversion only if the interface with a single abstract method is defined in Java. SAM conversion is not fully supported in the following scenarios:

  • The interface or abstract class is written in Kotlin
  • The method body refers to the interface itself by this

When we are converting code from Java to Kotlin incrementally, we encountered a few interesting scenarios where the definition of SAM in Kotlin is more verbose than written in Java 🤦🏻‍♀️. Let’s first take a look at different ways to define SAM:

Interface definition

Java

An organic SAM in Java is merely an interface with a single abstract method. We can add @FunctionalInterface to the interface to enable IDE checks from IntelliJ. Alternatively, we can extend Function from Java standard library.

Note that Function defines additional default methods likecompose and andThen. As long as the interface only has one abstract method interface, @FunctionalInterface can still be applied!

Kotlin

The following is what it looks like we use the IntelliJ action to convert Java file to Kotlin file.

Interfaces in Kotlin can have method implementations. We do not need to add the modifier default as such in Java:

Note: To enable valid Java code consuming this interface, we are adding @JvmDefault annotation here. In addition, we need to target Java 1.8 for compilation with the additional compiler arg Xjvm-default. More details can be found here.

The recommended way of leveraging function as a first-class citizen is to represent the SAM as a function: (Int)->Boolean.

Optionally, we can also use typealias to associate the function with a name typealias KotlinFunction = (Int)->Boolean . However, Kotlin function does not have the flexibility of adding default methods.

Consumer code

Let’s compare the scenarios with (Java | Kotlin code) calling (Java Interface | Kotlin Interface | Kotlin Function).

Java code calling Java Interface

Java’s lambda expression and method reference can provide an instance of SAM. Thanks to the minimalistic syntax of lambda expression in Java, the code is the simplest ✨:

Java code calling Kotlin Interface

When the interface is defined in Kotlin, the consumer code in Java looks just as clean as the Java code with Java interface.

Java code calling Kotlin Function

Although typealias is not directly visible, Java is able to refer Kotlin Function as FunctionN<R> where N is the number of function arguments.

Even though we can use Java lambda here, Java compiler still need to know about FunctionN . In a multi-module environment, this means that the kotlin-std library has to be declared as a dependency, even if the containing module is a Java module.

Kotlin code calling Java Interface

This is the example to demonstrate SAM conversion with Kotlin:

There is a nuance here that if the method invokeJavaSam()is defined in Java, Kotlin lambda expression or method reference can infer the correct SAM type so that only the curly braces {} are necessary. If the method invokeJavaSam() is defined in Kotlin, then we have to provide explicit SAM type to compile successfully, but the code looks slightly more verbose this way.

Kotlin code calling Kotlin Interface

If Kotlin code is consuming Kotlin, the code looks quite verbose 🤯 as we have to declare a full anonymous object rather than using lambda expressions. You will feel the same when reading the following code:

Some may argue that we can override the operator invoke() so that we can use lambda expression for creating anonymous objects:

However, this only solves the problem of creating anonymous objects using a lambda expression. The method reference and trailing-lambda syntax still won’t be applicable. Therefore, such black magic to solve a partial set of problems is not recommended 🙅‍♂️🙅‍♀️.

Kotlin code calling Kotlin Function

This is the idiomatic way of using Kotlin, even the trailing lambda syntax works like a breeze 🌬️:

Recommendation

Converting SAM from Java to Kotlin

Less worry for you if you are not planning to convert SAM interface to Kotlin yet (say in an external dependency). One thing worth doing is to make sure the SAM is annotated with @FunctionalInterfaceso that IntelliJ can validate if the annotated target always satisfies the definition of a SAM.

If you are writing new code in Kotlin, or converting a SAM interface which is used only at a few places, leverage the Kotlin function as a first-class citizen for both definition and consumption. Kotlin function works perfectly with all other features like type alias, lambda expression, method reference, and trailing-lambda syntax.

Now things are a bit more complicated if we are converting code from Java to Kotlin when the SAM interface is used widely in a large codebase:

  • To minimize the disruption in a large codebase where an interface could have hundreds of usages, use Kotlin interface instead of Kotlin function; convert interface code to Kotlin first, then consumer code to Kotlin.
  • Kotlin function requires the interface and consumption to be changed all at once because the type is changed in Java and the syntax is changed in Kotlin.
  • The option of “consumer code first, then interface code” may result in touching the consumer code twice: The first time to consume Java interface, you would use the SAM conversion in consumer code, such as JavaInterface { id -> id % 2 == 0 }, the second time to consume Kotlin interface, you need to update the syntax to be object : KotlinInterface { id -> id % 2 == 0 }.

Default methods

@JvmDefault was introduced in Kotlin 1.3 and is still experimental. Be aware of the binary compatibility and it requires targeting the Java code to 8. You would also have to depend on kotlin-stdlib-jdk8

Using default methods in Kotlin will be a dangerous move for an Android project as it does not fully support Java 8. You may unintentionally pass compilation using Java 8 features that cannot be desugared.

You can find the example code in this Github repo.

Android Developer@Instacart