skip to content

What do plain function types like (Int, Int) -> Int compile to on the JVM, and how does that relate to FunctionN interfaces and invoke?

level: middleimportance: should knowfreq 55%

answer

  1. (P1,P2)->R == Function2<P1,P2,R>
  2. Single operator fun invoke
  3. Function0..Function22, then vararg FunctionN
  4. in params / out return variance
  5. Lambda = object implementing FunctionN

basics

~10 s

A function type is really a regular interface in disguise. (Int, Int) -> Int becomes Function2<Int, Int, Int>, an interface with one method called invoke. Calling the function calls invoke.

solid answer

~40 s

Kotlin desugars an N-parameter function type to the library interface FunctionN, defined in kotlin.jvm.functions: Function0<R>, Function1<P1, R>, Function2<P1, P2, R>, and so on up to Function22. Each declares a single operator fun invoke(...): R. So (Int, Int) -> Int is exactly Function2<Int, Int, Int>. A lambda or function reference compiles to an object implementing the right FunctionN interface and overriding invoke; on the JVM the compiler typically synthesizes a class (or uses invokedynamic / LambdaMetafactory for SAM/Java interop). The call site f(a, b) is compiled to f.invoke(a, b). Because invoke is an operator, you get the parenthesis call syntax for free. Arities above 22 use FunctionN with a vararg-style invoke. This is why function types are ordinary reference types and can be nullable, used as generics, etc.

code

kotlin · 11 lines
kotlin
// A function type IS a FunctionN interface
val inc: (Int) -> Int = { it + 1 }
val asIface: Function1<Int, Int> = inc      // same type, compiles fine

// You can implement invoke by hand to make a callable object
class Multiplier(val factor: Int) : (Int) -> Int {
    override fun invoke(x: Int): Int = x * factor
}
val triple = Multiplier(3)
println(triple(5))        // 15, via invoke
println(triple.invoke(5)) // 15, explicit

go deeper

for a junior

Knows function types can be called and that invoke exists, even if hazy on FunctionN names.

for a middle

Names FunctionN / Function2, knows invoke is the single method and f(x) desugars to f.invoke(x).

for a senior

Explains variance (in/out), the 22-arity cap with vararg FunctionN, and that lambdas compile to objects implementing the interface (invokedynamic for SAM).

for a principal

Reasons about boxing/erasure, manual FunctionN implementations for stateful callables, and codegen trade-offs (synthetic class vs LambdaMetafactory).

## Function types are interfaces Kotlin has no special VM-level concept of a function value. Instead, every plain function type maps to one of the **`FunctionN` interfaces** from the standard library (package `kotlin`, JVM shapes in `kotlin.jvm.functions`): - `() -> R` → `Function0<R>` - `(P1) -> R` → `Function1<P1, R>` - `(P1, P2) -> R` → `Function2<P1, P2, R>` - … up to `Function22`. Beyond 22 parameters there is a generic `FunctionN` whose `invoke` takes a vararg. Each interface declares exactly one method: ```kotlin public interface Function2<in P1, in P2, out R> { public operator fun invoke(p1: P1, p2: P2): R } ``` Note the variance: parameters are `in` (contravariant), the return is `out` (covariant) — so `(Animal) -> Int` is usable where `(Cat) -> Number` is expected, matching the subtyping rules of functions. ## So this equivalence holds ```kotlin val add: (Int, Int) -> Int = { a, b -> a + b } val same: Function2<Int, Int, Int> = add // identical type ``` ## What a lambda compiles to A lambda or function reference becomes an **object that implements the matching `FunctionN` interface** and overrides `invoke`. On the JVM the compiler either synthesizes a class (often subclassing an internal `FunctionImpl`/`Lambda` base) or, for SAM conversions to Java interfaces, uses `invokedynamic` + `LambdaMetafactory`. Either way, the runtime value is a normal object. ## How calling works Because `invoke` is marked `operator`, the parenthesis-call syntax is sugar: ```kotlin add(2, 3) // compiles to add.invoke(2, 3) // this ``` You can write either form; they are the same call. ## Why this matters - Function types are ordinary **reference types**, so they can be nullable, stored in collections, used as type arguments, and implemented by hand. - You can implement a `FunctionN` interface manually to create a callable object with extra state or methods. - It explains why a value of `() -> Unit` is non-null by default and boxes parameters/return as objects (generics erase to `Object`).

  • What happens for a function type with more than 22 parameters?
    It maps to the generic FunctionN interface whose invoke takes a vararg Array<Any?>; the dedicated Function0..Function22 only cover up to 22 parameters.
  • Can you implement (Int) -> Int with a named class?
    Yes. Declaring class C : (Int) -> Int and overriding operator fun invoke(x: Int): Int gives a callable object, since (Int) -> Int is just Function1<Int, Int>.

saying these in an interview costs you the question

  • Saying function types are a special VM primitive with no interface backing
  • Forgetting that invoke is the single method behind the call syntax
  • Claiming there is one universal Function interface for all arities
  • Not knowing the FunctionN cap is 22 before vararg fallback

context