What surprising behaviors can SAM conversions cause around object identity, and how do you handle a Java method that takes a registered then-unregistered listener?
answer
- Each lambda literal -> distinct SAM instance
- add then remove with two literals = removal fails
- Convert once to a val, reuse the reference
- Non-capturing lambdas may be cached (don't rely on it)
- Identity matters for remove/equals/map keys
basics
~20 sEach SAM-converted lambda may become a different object instance, so you can't reliably remove a listener by passing the 'same' lambda again. Capture the converted instance in a variable and reuse that exact reference for both add and remove.
solid answer
~40 sSAM conversion wraps a lambda into a fresh interface instance at the conversion site. Two separate lambda literals — even with identical code — produce distinct objects, so addListener { ... } followed by removeListener { ... } passes two different instances and the removal silently fails. The fix is to convert once and reuse: val l = Runnable { ... } (or the listener interface), then add(l) and remove(l) with the same reference. Note Kotlin may cache a non-capturing converted lambda so repeated conversions of the same literal can return one shared instance, but this is an optimization you must not rely on for identity semantics. Capturing lambdas (closing over variables) generally create new instances. The principle: when identity matters (equals/removal/maps), hold the converted SAM instance explicitly rather than depending on the compiler.
code
kotlin · 12 linesimport java.awt.event.ActionListener
class Toolbar(private val button: javax.swing.JButton) {
private val onClick = ActionListener { println("clicked") }
fun enable() = button.addActionListener(onClick)
fun disable() = button.removeActionListener(onClick) // same ref -> removes it
}
// Anti-pattern (does NOT remove the listener):
// button.addActionListener { println("clicked") }
// button.removeActionListener { println("clicked") } // different objectgo deeper
Understands a lambda becomes an object but may not foresee the remove-listener bug.
Knows to store the converted instance in a val and reuse it for add/remove.
Explains capturing vs non-capturing identity, compiler caching as a non-guarantee, and equals/identity implications.
Anticipates these traps in framework/API design and chooses explicit instances or registration handles to make identity contracts safe.
## The identity trap A SAM conversion materializes an **interface instance** from a lambda. The critical consequence: **different lambda literals are different objects**, even if their source is identical. Many Java APIs are built around add/remove by reference: ```kotlin // Java: void addListener(ActionListener l); void removeListener(ActionListener l); button.addActionListener { handleClick() } button.removeActionListener { handleClick() } // BUG: removes nothing ``` The two lambdas are SAM-converted into **two separate** `ActionListener` objects. `removeActionListener` compares by reference (or `equals`, which defaults to identity for these wrappers), finds no match, and silently does nothing. ## The fix: convert once, reuse the reference Hold the converted instance and pass the **same** reference both times: ```kotlin val listener = ActionListener { handleClick() } button.addActionListener(listener) // ... later ... button.removeActionListener(listener) // works: same object ``` ## What about compiler caching? For a **non-capturing** lambda (one that doesn't close over any local variables or `this`), the compiler may emit a **singleton** and reuse the same instance across conversions of that literal. So sometimes two identical bare lambdas *happen* to be equal. **Do not rely on this** — it's an optimization, not a guarantee, and any captured state (a variable, a receiver) generally forces a fresh instance: ```kotlin fun register(tag: String) { // captures 'tag' -> typically a new instance each call bus.subscribe { log(tag) } } ``` ## Related pitfalls - **Using lambdas as map keys / set members:** identity-based equality means lookups can miss. Store the SAM instance. - **Anonymous object alternative:** `object : ActionListener { ... }` makes the single-instance intent explicit and is sometimes clearer when identity matters. - **No automatic deduplication:** SAM conversion never deduplicates across call sites for you. ## Mental model Treat a SAM conversion like `new Wrapper(lambda)` at each site. If you need stable identity (removal, equality, keys), bind the wrapper to a `val` and pass that. The compiler's caching is a bonus, never a contract. ## Bytecode note Under the hood, captured lambdas become classes with fields for the captured values; non-capturing ones can be singletons (often via `invokedynamic`/`LambdaMetafactory`). The captured-state distinction is exactly why identity varies.
- Why might two identical bare lambdas sometimes be the same instance?Non-capturing lambdas can be compiled to a cached singleton. It's an optimization you must not depend on for identity semantics.
- How does capturing a variable affect SAM-conversion identity?A capturing lambda holds the captured state in fields, so each conversion typically yields a new, distinct instance — the singleton optimization no longer applies.
Writing the same lambda twice is like signing two identical-looking but separate contracts; canceling one doesn't void the other — you must keep and reuse the one original you signed.
saying these in an interview costs you the question
- Assuming two identical lambdas are always the same object
- Calling remove with a fresh lambda literal and expecting it to work
- Relying on the non-capturing caching as a guarantee
- Not realizing capture forces new instances
- Using lambdas as map keys expecting structural equality