JavaRush /Courses /Kotlin SELF /SAM conversion — Kotlin lambdas as implementations of Jav...

SAM conversion — Kotlin lambdas as implementations of Java SAM types

Kotlin SELF
Level 29, Lesson 3
Available

1. Why SAM conversion is needed in Java APIs

If you look at the Java API through a beginner’s eyes, it can seem like Java developers really love “wrapping simple things into interfaces.” Sometimes that’s true, but historically this was the main way to pass behavior into a method: not with a lambda (as in Kotlin), but with an object that has a single method—“do an action.” Kotlin on the JVM lives next to Java, so it must be able to talk to such APIs cleanly, without forcing you to write verbose constructs every time.

SAM stands for Single Abstract Method—“one abstract method.” Such an interface is called a SAM type or a functional interface. The idea is simple: if an interface has exactly one abstract method, then instead of explicitly creating an implementation object, you can take a lambda and treat it as the implementation of that method. This is exactly what’s called SAM conversion.

Imagine that a Java method needs “something to execute.” In Java it often asks for a Runnable. In Kotlin you’d pass { ... }. SAM conversion is the mechanism that lets Kotlin say: “OK, I see that a Runnable is needed here. I’ll take the lambda and on the fly turn it into a Runnable object.”

A small diagram (in reality it’s a bit more complex, but this is enough to understand the idea):

flowchart LR
    A["Kotlin lambda { ... }"] --> B["SAM conversion (adapter)"]
    B --> C["Java interface Runnable/Comparator/Predicate"]
    C --> D["Java method calls the single abstract method"]

Which SAM types are most common in Java

Kotlin matches the “shape” of a lambda to the “shape” of the interface’s single abstract method: the number of parameters and the return type.

To navigate faster, it helps to keep in mind a few popular SAM types from Java and what they look like to a Kotlin developer:

Java SAM type Meaning Abstract method (conceptually) What a suitable Kotlin lambda looks like
Runnable
“do an action”
run(): void
() -> Unit
Comparator<T>
“compare two T values”
compare(a: T, b: T): int
(T, T) -> Int
Predicate<T>
“check T”
test(x: T): boolean
(T) -> Boolean
Consumer<T>
“process T”
accept(x: T): void
(T) -> Unit

Important: void in Java corresponds in meaning to Unit in Kotlin. That is, if Java expects a “function with no result,” the Kotlin lambda must not return a meaningful value.

2. Runnable: “run this”

When you first encounter Runnable, you might want to ask: “Why do I need a type just to execute code?” Answer: because that’s how many Java APIs are designed. And the good news is that Kotlin doesn’t force you to write a separate class or an anonymous object: a lambda is enough as long as the expected parameter type is known.

SAM conversion when assigning to a variable

Here Kotlin has to create a Runnable object, so we explicitly specify the variable type:

fun main() {
    val task: Runnable = Runnable {
        println("Running the task") // Running the task
    }

    task.run() // Running the task
}

Notice the form Runnable { ... }. It looks like a “constructor call,” even though Runnable is an interface. In Kotlin this is the familiar SAM-wrapper syntax: “take a lambda and make an object of the required SAM type out of it.”

SAM conversion directly in a method argument

When Kotlin sees that a method parameter is Runnable, it can apply SAM conversion automatically:

fun runNow(task: Runnable) {
    task.run()
}

fun main() {
    runNow {
        println("Running right now") // Running right now
    }
}

The nice part is that the syntax looks “Kotlin-ish”: we passed a lambda as the last argument (trailing lambda), and Kotlin figured out on its own that it’s a Runnable.

3. Collections: Predicate/removeIf and Comparator

To keep the examples grounded, we’ll evolve a small console app (no OOP—just functions and collections, like you already know). Let it be a rough “BudgetBuddy”: we store expenses as Triple(id, category, amount) so that we can sort, filter, and delete them.

Mini data example for “BudgetBuddy”

Let’s prepare some sample data:

import java.util.ArrayList

typealias Expense = Triple<Int, String, Int>

fun main() {
    val expenses = ArrayList<Expense>()
    expenses.add(Triple(1, "food", 800))
    expenses.add(Triple(2, "taxi", 450))
    expenses.add(Triple(3, "food", 120))

    println(expenses) // [(1, food, 800), (2, taxi, 450), (3, food, 120)]
}

Yes, we used java.util.ArrayList specifically, not mutableListOf(...). Not because you must always do so, but to see Java methods that use SAM types.

Predicate and removeIf: deleting elements via a lambda

Many Java collections have a removeIf(...) method that accepts a Predicate<T>: “remove the element if the predicate returned true.” This is a perfect example of SAM conversion: instead of a Predicate, we want to pass a lambda.

Let’s add a command to the app: “remove everything cheaper than a given amount.” For learning purposes, we’ll just call a function:

import java.util.ArrayList

typealias Expense = Triple<Int, String, Int>

fun removeCheaperThan(expenses: ArrayList<Expense>, minAmount: Int) {
    expenses.removeIf { expense ->
        val amount = expense.third
        amount < minAmount
    }
}

fun main() {
    val expenses = arrayListOf(
        Triple(1, "food", 800),
        Triple(2, "taxi", 450),
        Triple(3, "food", 120),
    )

    removeCheaperThan(expenses, 500)
    println(expenses) // [(1, food, 800)]
}

Key point: removeIf expects a Predicate<Expense>. We passed { expense -> ... }. Kotlin performed SAM conversion and created a Predicate object whose test(expense) method calls our lambda.

If you’ve ever written filter { ... } in Kotlin—conceptually it’s similar, but filter returns a new list, while removeIf mutates the existing one (a typical Java style).

Comparator as a SAM type: sorting

Sorting is a place where SAM conversion shows up constantly. In Kotlin you’ve already used sortedBy, sortedWith(compareBy(...)), and you may have noticed that Comparator is a central idea.

The Java interface Comparator<T> is a SAM type: it has one abstract method compare(a, b) that returns an Int. So we can create a comparator with a lambda.

Example: sort expenses by amount (the third component of Triple) in descending order:

import java.util.Comparator

typealias Expense = Triple<Int, String, Int>

fun main() {
    val expenses: List<Expense> = listOf(
        Triple(1, "food", 800),
        Triple(2, "taxi", 450),
        Triple(3, "food", 120),
    )

    val byAmountDesc: Comparator<Expense> = Comparator { a, b ->
        b.third - a.third
    }

    println(expenses.sortedWith(byAmountDesc))
    // [(1, food, 800), (2, taxi, 450), (3, food, 120)]
}

It matters not only that it “works,” but that you understand the contract: the comparator must return a negative number, zero, or a positive number depending on the order. The sign of the result determines “which one is greater.”

Be careful with b.third - a.third

Beginners often write b - a (like we did above), and it usually works for small numbers. But if values can be large, the difference can overflow. A safer style is to use compareTo:

import java.util.Comparator

typealias Expense = Triple<Int, String, Int>

fun main() {
    val byAmountDesc: Comparator<Expense> = Comparator { a, b ->
        b.third.compareTo(a.third)
    }

    val xs = listOf(Triple(1, "food", 2_000_000_000), Triple(2, "taxi", 1))
    println(xs.sortedWith(byAmountDesc))
    // [(1, food, 2000000000), (2, taxi, 1)]
}

This version is a bit longer, but it doesn’t turn sorting into a lottery of “did Int overflow or not?”

4. Function references and ambiguous overloads

:: lets you pass a function reference instead of a lambda. Sometimes it makes code not only shorter, but also more honest: “I’m not inventing new logic, I’m just using an existing function.”

There’s one nuance: for :: to work, the function signature must match the SAM type method.

SAM conversion + callable reference

Let’s make a named function to compare two expenses:

typealias Expense = Triple<Int, String, Int>

fun compareByCategoryThenAmount(a: Expense, b: Expense): Int {
    val byCategory = a.second.compareTo(b.second)
    return if (byCategory != 0) byCategory else a.third.compareTo(b.third)
}

Now let’s use it as the basis for a Comparator:

import java.util.Comparator

typealias Expense = Triple<Int, String, Int>

fun compareByCategoryThenAmount(a: Expense, b: Expense): Int {
    val byCategory = a.second.compareTo(b.second)
    return if (byCategory != 0) byCategory else a.third.compareTo(b.third)
}

fun main() {
    val cmp: Comparator<Expense> = Comparator(::compareByCategoryThenAmount)

    val xs = listOf(
        Triple(1, "taxi", 450),
        Triple(2, "food", 800),
        Triple(3, "food", 120),
    )

    println(xs.sortedWith(cmp))
    // [(3, food, 120), (2, food, 800), (1, taxi, 450)]
}

Here Comparator(::compareByCategoryThenAmount) is SAM conversion as well; the behavior source is not a lambda but a callable reference.

When the compiler can’t tell which SAM you meant

In real code, SAM conversion sometimes breaks not because Kotlin is “bad,” but because we gave it too little information. The most common symptom is: you write { ... }, and the compiler responds with something like “cannot choose an overload” or “cannot infer a type for this parameter.”

To see the problem on a minimal example, let’s make two functions with the same name but different SAM types:

import java.util.concurrent.Callable

fun runAction(action: Runnable) {
    println("Runnable")
    action.run()
}

fun runAction(action: Callable<String>) {
    println("Callable")
    println(action.call())
}

Now let’s try to call it:

fun main() {
    runAction { "hello" }
}

From a human perspective, this is “return the string hello.” But from the compiler’s perspective, this potentially matches Callable<String> (it has a return value) and does not match Runnable (it corresponds to Unit). In a different situation it can be the other way around—and you can unexpectedly land in the wrong overload.

The solution in such cases is simple and very practical: make your intent explicit.

Here is how we explicitly choose Runnable:

fun main() {
    runAction(Runnable {
        println("Just executing") // Just executing
    })
}

And here is how we explicitly choose Callable<String>:

import java.util.concurrent.Callable

fun main() {
    runAction(Callable {
        "hello"
    })
}

Yes, it looks a bit more verbose. But you no longer play “guess what I meant” with the compiler. In production code, this “extra noise” often saves hours of debugging.

5. Closures in SAM lambdas and state capture

Lambdas in Kotlin can capture outer variables—you’ve already seen that. SAM conversion changes nothing: inside Predicate { ... } or Runnable { ... } you can still use outer val/var.

This is convenient, but there’s a risk: you create a handler that looks like a “pure function,” but in fact depends on mutable state.

Let’s look at an example with removeIf that uses an outer var:

import java.util.ArrayList

typealias Expense = Triple<Int, String, Int>

fun main() {
    val expenses = ArrayList<Expense>()
    expenses.add(Triple(1, "food", 800))
    expenses.add(Triple(2, "taxi", 450))

    var minAmount = 500

    expenses.removeIf { it.third < minAmount }
    println(expenses) // [(1, food, 800)]

    minAmount = 900
    expenses.removeIf { it.third < minAmount }
    println(expenses) // []
}

Technically, everything is correct. But if such a Predicate lives for a long time (for example, it’s stored somewhere in a variable and used later), then the “outer var” becomes an unexpected control lever. This is especially painful for beginners, because the code doesn’t make it obvious that the handler depends on state.

A practical habit that helps: if the value truly should be fixed for the predicate, store it in a val before creating the lambda (i.e., take a “snapshot”), rather than using a “live” var.

6. Typical mistakes with SAM conversion

Error No. 1: assuming “any lambda automatically fits anywhere.”
SAM conversion works only when the expected type is exactly a SAM interface and the lambda signature matches the single abstract method. If a method expects a regular object, or the interface has more than one abstract method, a lambda won’t help—you’ll need another approach.

Error No. 2: missing the difference between a Kotlin functional type and a Java functional interface.
The type (Int) -> Boolean and the type Predicate<Int> are similar in meaning, but they are different entities. In Kotlin code you can store a predicate as (Int) -> Boolean, but a Java method may require exactly Predicate<Int>. That’s where SAM conversion is needed; otherwise the types don’t “connect.”

Error No. 3: writing a comparator as b - a and accidentally getting overflow.
For small numbers the difference works, but for large values Int can overflow, and sorting starts behaving strangely. It’s more reliable to use compareTo, even if it feels like “it’s fine as is.”

Error No. 4: hitting overload ambiguity and trying to “convince” the compiler with hints.
If you have multiple overloaded methods/functions and a lambda fits more than one option, the compiler must ask “which one exactly?” The fix is not magic parentheses or rearranging spaces, but an explicit type: either via a variable of the desired SAM type, or via a wrapper like Runnable { ... }, Comparator { ... }.

Error No. 5: silently capturing an outer var and getting “hidden state.”
A SAM lambda can easily become a handler with non-obvious dependencies if it uses an outer mutable variable. In small examples it’s not scary, but in a real app this behavior makes debugging harder: the result depends not only on input parameters, but also on whether someone changed var somewhere before the handler was invoked.

1
Task
Kotlin SELF, level 29, lesson 3
Locked
The standby Runnable
The standby Runnable
1
Task
Kotlin SELF, level 29, lesson 3
Locked
Run wrapper
Run wrapper
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION