kotlin language guide classes & interfaces special interfaces

Functional (SAM) interfaces,
explained properly.

One abstract method, one keyword, and a compiler trick that turns ceremony into a lambda. A step-by-step guide to fun interface — what it is, what the compiler does with it, and when a plain type alias is the better tool.

fun interface IntPredicate { fun accept(i: Int): Boolean }

01The concept

A name for a very specific shape of interface.

An interface with exactly one abstract member function is called a functional interface, or a SAM interface — Single Abstract Method. It may carry any number of non-abstract members (functions with bodies), but only one method can be left abstract.

In Kotlin you opt in explicitly with the fun modifier on the interface declaration:

KotlinKRunnable.kt
fun interface KRunnable {
    fun invoke()
}

That little fun is a contract with the compiler. It says: this interface will always have exactly one abstract method, so let callers implement it with a lambda. The compiler enforces the contract — add a second abstract method and the declaration stops compiling.

Why "SAM"? The term comes from the Java world, where interfaces like Runnable, Comparator, and Callable all follow the same single-abstract-method shape. Kotlin borrows both the name and the conversion mechanic — and extends it to Kotlin-defined interfaces.

02Your first one, step by step

From a plain interface to a one-liner, in five moves.

1

Start with an ordinary interface

Say you want a reusable "does this Int pass?" check. The classic OO answer is an interface:

Kotlinstep 1 — plain interface
interface IntPredicate {
    fun accept(i: Int): Boolean
}
2

Feel the pain of implementing it

Without any help from the language, every implementation is an anonymous object — six lines to express "is it even":

Kotlinstep 2 — the long way
// Creating an instance of a class
val isEven = object : IntPredicate {
    override fun accept(i: Int): Boolean {
        return i % 2 == 0
    }
}
3

Add the fun modifier

One keyword changes what callers are allowed to do:

Kotlinstep 3 — make it functional
fun interface IntPredicate {
    fun accept(i: Int): Boolean
}
4

Implement it with a lambda

Now the six lines collapse into one. The lambda's shape — takes an Int, returns a Boolean — matches the single abstract method, so the compiler wires it up for you:

Kotlinstep 4 — SAM conversion
// Creating an instance using a lambda
val isEven = IntPredicate { it % 2 == 0 }
5

Use it like any other object

The result is a real IntPredicate instance — you call its method by name:

Kotlinstep 5 — run it
fun main() {
    println("Is 7 even? - ${isEven.accept(7)}")
}

// Output:
// Is 7 even? - false

03SAM conversion, up close

What the compiler actually does when you hand it a lambda.

The one-liner in step 4 isn't magic syntax — it's a SAM conversion. When a lambda's signature matches the signature of a functional interface's single method, Kotlin converts the lambda into code that dynamically instantiates an implementation of that interface. Conceptually, you write the left tab and the compiler produces something equivalent to the right tab:

val isEven = IntPredicate { it % 2 == 0 }
// one expression, no override, no class body
val isEven = object : IntPredicate {
    override fun accept(i: Int): Boolean {
        return i % 2 == 0
    }
}
// an instance of an anonymous implementation, generated for you

Two things follow from this mental model:

a

The lambda must match the method's signature

Parameter types and return type are checked against the single abstract method. If they don't line up, there is nothing to convert and you get a type error.

b

IntPredicate { ... } is a SAM constructor

Writing the interface name before the lambda is an explicit conversion. In parameter position it's often unnecessary — if a function expects an IntPredicate, you can pass a bare lambda and the conversion happens implicitly:

Kotlinimplicit conversion at a call site
fun countMatching(numbers: List<Int>, predicate: IntPredicate): Int =
    numbers.count { predicate.accept(it) }

fun main() {
    val nums = listOf(1, 2, 3, 4, 5, 6)

    // bare lambda: converted implicitly because the parameter type is known
    println(countMatching(nums) { it % 2 == 0 })   // 3

    // works with function references too
    println(countMatching(nums, ::isPositive))        // 6
}

fun isPositive(i: Int): Boolean = i > 0

04Rules & gotchas

What the compiler will and won't let you get away with.

Exactly one abstract method — but non-abstract members are fine

"Functional" doesn't mean "single-member." The interface can carry helper functions with bodies; only the abstract count is restricted:

Kotlinnon-abstract members are allowed
fun interface Validator {
    fun validate(input: String): Boolean          // the one abstract method

    fun validateOrThrow(input: String) {           // has a body — fine
        require(validate(input)) { "Invalid: $input" }
    }
}

Two abstract methods? Compile error.

Kotlinwhat breaks the contract
fun interface Broken {
    fun first()
    fun second()   // error: fun interface must have exactly one abstract method
}
Gotcha Abstract properties also break the contract. A fun interface can't declare val name: String without an implementation — there'd be no way to express it in a lambda.

They're real types: extension and inheritance work

Because a functional interface creates a genuine new type, it can extend other interfaces and be the target of extension functions — capabilities you'll lean on in section 08.

Cost model A SAM conversion instantiates an implementation of the interface. That's usually negligible, but it is not free the way a plain function type is — keep this in mind on hot paths, and see the type-alias comparison for when to skip the interface entirely.

05Java interop

You've been using SAM conversions all along.

SAM conversion also applies to Java interfaces with a single abstract method — no fun modifier needed, since Java interfaces can't opt in and Kotlin extends the courtesy automatically. Every time you've passed a lambda where a Java API wanted a Runnable or Comparator, this is what happened:

Kotlincalling Java APIs
import java.util.concurrent.Executors

fun main() {
    val executor = Executors.newSingleThreadExecutor()

    // java.lang.Runnable is a SAM interface — lambda converts implicitly
    executor.submit { println("running on ${Thread.currentThread().name}") }

    // java.util.Comparator too
    val byLength = Comparator<String> { a, b -> a.length - b.length }
    println(listOf("kotlin", "jvm", "sam").sortedWith(byLength))

    executor.shutdown()
}

A useful bit of history: Kotlin supported SAM conversion for Java interfaces from the very beginning, but for Kotlin's own interfaces you must opt in with fun interface. The reason is that Kotlin already has first-class function types — the language wants you to be deliberate about choosing interface semantics over a plain (Int) -> Boolean.

06Migrating legacy patterns

Retiring the "interface + factory function" idiom without breaking callers.

Before fun interface existed, a common workaround was to pair a plain interface with a same-named factory function that accepted a lambda:

1

The legacy shape

Kotlinbefore — interface + constructor function
interface Printer {
    fun print()
}

fun Printer(block: () -> Unit): Printer = object : Printer {
    override fun print() = block()
}

Callers could then write Printer { ... } or pass ::Printer as a reference — mimicking what SAM constructors now give you for free.

2

Replace both with one declaration

Since Kotlin 1.6.20, callable references to functional interface constructors are supported, making the migration source-compatible. The whole pattern collapses to:

Kotlinafter — just a fun interface
fun interface Printer {
    fun print()
}

Its constructor is created implicitly, so code that used the ::Printer function reference keeps compiling:

Kotlinexisting call sites still work
documentsStorage.addPrinter(::Printer)
3

Keep binary compatibility for compiled callers

Source compatibility isn't binary compatibility — already-compiled libraries still link against the old factory function. Keep it in the bytecode but hide it from new source with DeprecationLevel.HIDDEN:

Kotlinhide the legacy factory
@Deprecated(
    message = "Use the fun interface constructor instead",
    level = DeprecationLevel.HIDDEN
)
fun Printer(block: () -> Unit): Printer = Printer(block)

HIDDEN makes the declaration invisible to newly compiled code while keeping the symbol available for binaries that already reference it.

07Functional interfaces vs. type aliases

Same shape at the call site — completely different type semantics.

The IntPredicate example can be rewritten with a type alias for a functional type, and at a glance it looks almost identical:

fun interface — new type

Kotlin
fun interface IntPredicate {
    fun accept(i: Int): Boolean
}

val isEven =
    IntPredicate { it % 2 == 0 }

// invoked via the method name
isEven.accept(7)

typealias — just a name

Kotlin
typealias IntPredicate =
    (i: Int) -> Boolean

val isEven: IntPredicate =
    { it % 2 == 0 }

// invoked directly
isEven(7)

The difference is in what exists afterward. A type alias is just a name for an existing typeIntPredicate and (Int) -> Boolean are the same type, fully interchangeable. A functional interface creates a new nominal type, and that buys real capabilities:

Capabilitytypealiasfun interface
Creates a new type No — a rename only Yes — a distinct nominal type
Members One (the function shape itself) One abstract method plus any non-abstract members
Can extend / implement other interfaces No Yes
Type-specific extension functions No — they'd apply to every matching function Yes — scoped to this interface only
Runtime cost None beyond the function itself May require conversion to the interface type
Call syntax isEven(7) isEven.accept(7)

The decision, in two questions

Does your API just need to accept any function of a given shape?
Use a plain functional type — or a typealias to give that type a shorter, readable name. Cheapest option, zero conversion cost, maximum interchangeability.
Does your API accept something more than a function — a contract, a domain identity, or operations that don't fit in a signature?
Declare a fun interface. You get a real type to hang extensions on, default helper methods, and interface inheritance — at the price of slightly more syntax and possible conversion cost.

08Patterns in practice

Where fun interfaces earn their keep in real codebases.

Pattern 1 — Pluggable strategies with lambda-friendly call sites

A functional interface gives the dependency a domain name and a documented contract, while callers still get to pass a one-line lambda:

Kotlinstrategy injection
fun interface RetryPolicy {
    /** Returns the delay in ms before [attempt], or null to stop retrying. */
    fun delayFor(attempt: Int): Long?
}

class WebhookDispatcher(private val retryPolicy: RetryPolicy) {
    // ... uses retryPolicy.delayFor(attempt) between deliveries
}

// Call sites read like configuration:
val dispatcher = WebhookDispatcher { attempt ->
    if (attempt <= 5) 1000L * (1 shl attempt) else null   // exponential backoff, 5 tries
}

Pattern 2 — Composition through extension functions

This is the capability a type alias can't give you: extensions scoped to this interface, not to every (String) -> Boolean in the program:

Kotlincomposable validators
fun interface Validator {
    fun validate(input: String): Boolean
}

// Extensions specific to Validator — impossible on a bare function type
infix fun Validator.and(other: Validator) =
    Validator { validate(it) && other.validate(it) }

infix fun Validator.or(other: Validator) =
    Validator { validate(it) || other.validate(it) }

fun main() {
    val notBlank  = Validator { it.isNotBlank() }
    val maxLength = Validator { it.length <= 64 }
    val looksLikeEmail = Validator { "@" in it }

    val emailField = notBlank and maxLength and looksLikeEmail

    println(emailField.validate("erick@example.com"))  // true
    println(emailField.validate(""))                    // false
}

Pattern 3 — A contract richer than a signature

When the single method carries non-trivial semantics — ordering guarantees, idempotency expectations, threading rules — the interface is the right home for that documentation. The signature (Event) -> Unit says nothing; EventHandler with a KDoc contract says everything:

Kotlincontract as documentation
fun interface EventHandler {
    /**
     * Handles a single event.
     *
     * Implementations MUST be idempotent: the same event may be
     * delivered more than once. Called from a single consumer
     * thread — no internal synchronization required.
     */
    fun handle(event: Event)
}
Rule of thumb Reach for function types by default, promote to a typealias when the type gets long or repeats, and promote to a fun interface the moment the parameter stops being "any function" and starts being "a thing in the domain."