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 therun()
methodComparator
with thecompare()
methodOnClickListener
with theonClick()
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:
// 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:
// 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:
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:
// 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:
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
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
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
// 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:
-
Java interfaces only (by default): SAM conversions automatically work with Java interfaces, but you need to use the
fun interface
declaration for Kotlin interfaces. -
One abstract method: The interface must have only one abstract method. If there are multiple abstract methods, SAM conversion won't work.
-
Variable capture: Lambda expressions can capture variables from their containing scope, which isn't possible with traditional anonymous classes in Java.
-
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
-
Convert the following anonymous object implementation to use a SAM conversion:
kotlinval comparator = object : Comparator<String> {
override fun compare(s1: String, s2: String): Int {
return s1.length - s2.length
}
} -
Create a custom functional interface called
DataProcessor
with a methodprocess(data: String): Int
that returns the length of the processed data. Then use it with a SAM conversion. -
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! :)