skip to content

When mixing Kotlin and Java, how do SAM conversions, Kotlin properties, and the read-only/mutable collection split behave across the boundary?

level: seniorimportance: nice to knowfreq 38%

answer

  1. SAM conversion: lambda -> Java functional interface, auto
  2. Kotlin interface needs 'fun interface' for lambda
  3. Property -> getX/setX; Java accessors -> synthetic property
  4. List read-only is compile-time only; runtime is java.util.List
  5. Java can mutate a Kotlin read-only List -> defensive copy

basics

~20 s

A Kotlin lambda can stand in for a Java single-method interface (SAM conversion). Kotlin properties show up as getX/setX to Java. And Kotlin's read-only vs mutable list distinction disappears at runtime, so Java can still mutate a 'read-only' list.

solid answer

~40 s

**SAM conversion** lets you pass a Kotlin lambda where Java expects a **functional (single-abstract-method) interface** like `Runnable` or `Comparator`; the compiler wraps the lambda into an instance. This works automatically for *Java* SAM interfaces; for *Kotlin* interfaces you need `fun interface` to opt in. **Properties:** a Kotlin `var x` is seen by Java as `getX()`/`setX()` (and `isX()` for `Boolean` named `isX`); conversely Java getter/setter pairs are exposed to Kotlin as synthetic properties you can access with property syntax. **Collections:** Kotlin's `List` (read-only) vs `MutableList` is a compile-time-only distinction — both are `java.util.List` at runtime, so Java code handed a Kotlin read-only `List` can still call `add()` and mutate it (a known boundary pitfall). Defensive copies or wrapping guard against this.

code

kotlin · 15 lines
kotlin
// SAM: Kotlin lambda for Java Runnable
Thread { println("hi") }.start()

// fun interface enables lambda for a Kotlin interface
fun interface Validator { fun ok(s: String): Boolean }
val v = Validator { it.isNotBlank() }

// Java accessors as synthetic property
// Java: class C { String getName(){...} void setName(String n){...} }
val c = C()
c.name = "x"; println(c.name)

// Read-only is not enforced at runtime
val ro: List<Int> = listOf(1)
// (java.util.List) ro can be mutated by Java code

go deeper

for a junior

Knows a lambda can be passed to a Java Runnable/Comparator and properties become getters.

for a middle

Explains fun interface for Kotlin SAM and getX/setX exposure both directions.

for a senior

Identifies the read-only collection leak across the boundary and how to defend against it.

for a principal

Designs boundary contracts with defensive copies/immutable types and codifies SAM/property conventions for mixed teams.

## SAM conversion A **SAM (Single Abstract Method) interface** — also called a **functional interface** — is an interface with exactly one abstract method (e.g. `Runnable`, `Comparator<T>`, `Callable<V>`). **SAM conversion** is the compiler turning a lambda into an instance of such an interface. ```kotlin // Java method: void execute(Runnable r) executor.execute { println("run") } // lambda -> Runnable via SAM conversion // Java: list.sort(Comparator<String>) list.sortWith(compareBy { it.length }) // or pass a lambda where a Comparator is expected ``` Key rule: SAM conversion happens **automatically for Java interfaces**. For a **Kotlin** interface you must declare it `fun interface` to enable lambda conversion; otherwise you must use an explicit `object : MyInterface { ... }`. ```kotlin fun interface Handler { fun handle(x: Int) } // now: Handler { ... } works ``` ## Properties across the boundary Kotlin **properties** compile to accessor methods, so Java sees the JVM-bean shape: - `val name: String` → `getName()` - `var count: Int` → `getCount()` + `setCount(int)` - `val isReady: Boolean` (named with `is`) → `isReady()` (no `get` prefix) Going the other way, when **Kotlin consumes Java**, a Java `getX()/setX()` pair is exposed as a **synthetic property**: Kotlin lets you write `obj.x` / `obj.x = v` instead of `obj.getX()`/`obj.setX()`. A lone `getX()` with no setter becomes a read-only synthetic property. ```kotlin // Java: class Box { int getValue(); void setValue(int v); } val box = Box() box.value = 5 // calls setValue(5) println(box.value) // calls getValue() ``` ## Read-only vs mutable collections Kotlin splits collection interfaces: - `List`, `Set`, `Map` = **read-only views** (no mutators in the Kotlin interface) - `MutableList`, `MutableSet`, `MutableMap` = mutable But this split is **purely a Kotlin compile-time distinction**. At runtime both map to the same `java.util.List`/`Set`/`Map`. Consequences: - **Java can mutate a Kotlin read-only collection.** If you hand a Kotlin `List` to Java, Java sees `java.util.List` and can call `add()`/`remove()` — Kotlin's read-only contract gives **no runtime protection**. - A Java method returning `java.util.List` becomes a **platform collection type** in Kotlin (`(Mutable)List<...>!`); Kotlin won't enforce read-only-ness on it either. ```kotlin // Kotlin val ro: List<Int> = listOf(1, 2) legacy.feed(ro) // Java void feed(java.util.List l) { l.add(3); } // mutates ro's backing! ``` **Defenses:** pass a defensive copy (`ro.toList()` still won't help if backing is shared — use `ArrayList(ro)` / immutable structures), or wrap with `Collections.unmodifiableList(...)` so mutation throws. ## Mental model The boundary is where Kotlin's *source-level guarantees* (read-only, properties, functional types) meet the JVM's *flatter reality*. Most map cleanly (properties ⇄ accessors, lambdas ⇄ SAM), but read-only collections are the leaky abstraction: a Kotlin-only fiction that Java can ignore. Design boundary APIs with that in mind.

  • Why doesn't Kotlin's List type protect a collection from Java mutation?
    Read-only vs mutable is a Kotlin compile-time interface split; at runtime both are java.util.List, which has add/remove, so Java can mutate freely.
  • What's required to pass a lambda for a Kotlin (not Java) single-method interface?
    Declare it as a 'fun interface'. Plain Kotlin interfaces don't get automatic SAM conversion.

saying these in an interview costs you the question

  • Claiming Kotlin read-only List is immutable/protected at runtime
  • Saying SAM conversion works automatically for all Kotlin interfaces
  • Not knowing Java accessor pairs become Kotlin synthetic properties
  • Forgetting isX Boolean naming maps to isX() not getIsX()

context