Skip to main content

Kotlin SAM Conversions

Introduction

When you're working with Kotlin, especially when interacting with Java libraries, you'll often encounter interfaces with just a single method. These are called Single Abstract Method (SAM) interfaces, or functional interfaces in Java 8 terminology.

Kotlin provides a feature called SAM conversions that allows you to pass a lambda expression where a SAM interface is expected. This makes your code more concise and readable by eliminating the boilerplate of anonymous class creation.

Let's explore what SAM conversions are and how they can simplify your code.

What is a SAM Interface?

A SAM interface (Single Abstract Method interface) is an interface that contains just one abstract method. Some examples include:

  • Runnable with the run() method
  • Comparator with the compare() method
  • OnClickListener with the onClick() method

In Java 8 and above, these are also known as functional interfaces and are often annotated with @FunctionalInterface.

How SAM Conversions Work in Kotlin

Kotlin allows you to pass a lambda expression wherever a SAM interface is expected. The compiler automatically converts your lambda into an instance of that interface, implementing its single method with your lambda's body.

Let's see a simple example with the Runnable interface from Java:

kotlin
// Without SAM conversion (verbose)
val runnable = object : Runnable {
override fun run() {
println("Running without SAM conversion")
}
}

// With SAM conversion (concise)
val betterRunnable = Runnable {
println("Running with SAM conversion")
}

fun main() {
runnable.run()
betterRunnable.run()
}

Output:

Running without SAM conversion
Running with SAM conversion

As you can see, using SAM conversion makes our code much cleaner and more readable!

Using SAM Conversions with Java Methods

SAM conversions really shine when working with Java libraries that use callbacks or listeners. For instance, consider a Java button click listener:

kotlin
// Java method from Android SDK
// button.setOnClickListener(OnClickListener listener)

// Without SAM conversion
button.setOnClickListener(object : OnClickListener {
override fun onClick(view: View) {
println("Button clicked")
}
})

// With SAM conversion
button.setOnClickListener { view ->
println("Button clicked")
}

SAM Conversions with Method References

You can also use method references with SAM conversions for even more concise code:

kotlin
fun handleClick(view: View) {
println("Button clicked")
}

// Using a method reference with SAM conversion
button.setOnClickListener(::handleClick)

SAM Conversions with Kotlin Interfaces

An important thing to note is that SAM conversions work automatically for Java interfaces, but not for Kotlin interfaces. This is because in Kotlin, interfaces can have default implementations, making the concept of a "single abstract method" less clear.

Let's see the difference:

kotlin
// A Java interface (from java.lang package)
fun javaInterface() {
val runnable = Runnable { println("Java SAM interface") }
Thread(runnable).start() // SAM conversion works!
}

// A Kotlin interface
interface KotlinRunnable {
fun execute()
}

fun kotlinInterface() {
// This won't compile!
// val runner = KotlinRunnable { println("Kotlin interface") }

// You must use an object expression
val runner = object : KotlinRunnable {
override fun execute() {
println("Kotlin interface")
}
}
}

To enable SAM conversions for Kotlin interfaces, you can use the fun modifier when defining your interface:

kotlin
fun interface KotlinRunnable {
fun execute()
}

fun kotlinFunctionalInterface() {
// Now SAM conversion works!
val runner = KotlinRunnable { println("Kotlin functional interface") }
runner.execute()
}

Real-World Examples

Example 1: Working with threads

kotlin
fun createAndStartThread() {
// Using SAM conversion for Runnable
val thread = Thread {
for (i in 1..5) {
println("Thread is running: step $i")
Thread.sleep(1000)
}
}
thread.start()
}

fun main() {
createAndStartThread()
println("Main thread continues execution")
}

Output:

Main thread continues execution
Thread is running: step 1
Thread is running: step 2
Thread is running: step 3
Thread is running: step 4
Thread is running: step 5

Example 2: Sorting a list with Comparator

kotlin
data class Person(val name: String, val age: Int)

fun main() {
val people = listOf(
Person("Alice", 29),
Person("Bob", 31),
Person("Charlie", 25)
)

// Using SAM conversion for Comparator
val sortedByAge = people.sortedWith(Comparator { p1, p2 ->
p1.age - p2.age
})

println("People sorted by age:")
sortedByAge.forEach { println("${it.name}: ${it.age}") }

// Even shorter with Kotlin's comparison functions
val sortedByName = people.sortedWith(compareBy { it.name })

println("\nPeople sorted by name:")
sortedByName.forEach { println("${it.name}: ${it.age}") }
}

Output:

People sorted by age:
Charlie: 25
Alice: 29
Bob: 31

People sorted by name:
Alice: 29
Bob: 31
Charlie: 25

Example 3: Creating a custom listener with a Kotlin functional interface

kotlin
// Define a custom functional interface
fun interface OnTaskCompleteListener {
fun onTaskComplete(result: String)
}

class TaskManager {
fun executeTask(task: String, listener: OnTaskCompleteListener) {
// Simulate task execution
println("Executing task: $task")
Thread.sleep(1000)

// Notify listener when complete
listener.onTaskComplete("Task '$task' completed successfully")
}
}

fun main() {
val taskManager = TaskManager()

// Using SAM conversion with our functional interface
taskManager.executeTask("Data backup") { result ->
println("Received callback: $result")
}

// Multiple tasks with different handlers
taskManager.executeTask("File download") { result ->
println("Download finished: $result")
}
}

Output:

Executing task: Data backup
Received callback: Task 'Data backup' completed successfully
Executing task: File download
Download finished: Task 'File download' completed successfully

Limitations and Considerations

When using SAM conversions, keep these points in mind:

  1. Java interfaces only (by default): SAM conversions automatically work with Java interfaces, but you need to use the fun interface declaration for Kotlin interfaces.

  2. One abstract method: The interface must have only one abstract method. If there are multiple abstract methods, SAM conversion won't work.

  3. Variable capture: Lambda expressions can capture variables from their containing scope, which isn't possible with traditional anonymous classes in Java.

  4. Method overloading: When a method is overloaded with different SAM interfaces as parameters, you might need to provide explicit type information to help the compiler choose the right method.

Summary

SAM conversions are a powerful feature in Kotlin that allow you to:

  • Replace verbose anonymous class instances with concise lambda expressions
  • Write cleaner code when working with Java libraries
  • Create your own functional interfaces in Kotlin with the fun interface declaration

By leveraging SAM conversions, you can significantly reduce boilerplate code and make your codebase more readable and maintainable, especially when interacting with callback-based APIs or event listeners.

Exercises

  1. Convert the following anonymous object implementation to use a SAM conversion:

    kotlin
    val comparator = object : Comparator<String> {
    override fun compare(s1: String, s2: String): Int {
    return s1.length - s2.length
    }
    }
  2. Create a custom functional interface called DataProcessor with a method process(data: String): Int that returns the length of the processed data. Then use it with a SAM conversion.

  3. Implement a button click listener using SAM conversion that displays different messages based on how many times the button has been clicked.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)