skip to content

Extensions don't modify the class, so how does Kotlin compile an extension function under the hood?

level: middleimportance: must knowfreq 70%

answer

  1. Compiles to a static method
  2. Receiver = first (hidden) parameter
  3. Resolved on static/declared type, not runtime type
  4. Not virtual, cannot be overridden
  5. Called from Java as FileKt.fn(receiver)

basics

~10 s

Kotlin compiles an extension into a plain static method. The object you call it on is just passed as the first hidden argument. So a.foo() really becomes Foo.foo(a) at the bytecode level.

solid answer

~40 s

An extension function is **resolved statically** and compiled to a **static method** whose first parameter is the receiver. A top-level `fun String.shout()` in `Utils.kt` becomes (roughly) `public static String shout(String $this$shout)` in a `UtilsKt` class. The call `"hi".shout()` compiles to `UtilsKt.shout("hi")`. Because it is a static dispatch on the *declared (static) type* of the receiver, extensions are **not virtual** — they cannot be overridden and don't participate in polymorphism the way member functions do. This also explains why an extension can only use the public API of the receiver: from the JVM's view it is just an outside function holding a reference. The receiver-as-first-arg shape is also why Kotlin extensions are easy to call from Java: `UtilsKt.shout("hi")`.

code

kotlin · 10 lines
kotlin
open class Animal
class Dog : Animal()

fun Animal.label() = "animal"
fun Dog.label() = "dog"

fun main() {
    val a: Animal = Dog()
    println(a.label()) // "animal" — static type Animal decides, not runtime Dog
}

go deeper

for a junior

Knows the class isn't really modified and the receiver is passed in somehow.

for a middle

States it compiles to a static method with receiver as first param and is resolved on the static type.

for a senior

Connects static dispatch to non-overridability, public-API-only access, and clean Java interop.

for a principal

Reasons about API evolution risk: extensions are not virtual, so they can't be specialized by subtypes and may surprise consumers expecting polymorphism.

## The core fact: static resolution Kotlin **resolves extension functions statically** — the decision of *which* function to call is made at compile time from the **declared (compile-time) type** of the receiver expression, not the runtime type. There is no virtual dispatch table involved. ## What the bytecode looks like A top-level extension is compiled into a **static method** on a file-derived class. The receiver becomes the **first parameter**: ```kotlin // File: StringUtils.kt package com.example fun String.shout(): String = this.uppercase() + "!" ``` Conceptually compiles to (Java view): ```java public final class StringUtilsKt { public static String shout(String $receiver) { return $receiver.toUpperCase() + "!"; } } ``` And the call site: ```kotlin "hi".shout() // Kotlin // becomes StringUtilsKt.shout("hi"); // bytecode / Java view ``` The synthetic parameter is the receiver; inside the Kotlin body `this` maps to that parameter. ## Consequences of static dispatch - **Not polymorphic / not overridable.** Because dispatch uses the static type, an extension is selected by what the compiler *thinks* the expression is, not the object's real class. Extensions cannot be `override`n. - **Only public API access.** From the JVM's perspective the method lives outside the class, so it cannot touch `private` members of the receiver type — exactly the visibility you'd expect from a normal external static call. - **Java interop is clean.** Java code calls `StringUtilsKt.shout("hi")`. The `@JvmName` annotation can rename the generated file class if needed. - **Null receivers work** when the receiver type is nullable, because nothing is dereferenced implicitly — the value is simply passed as the argument (the body must handle null). (Detailed nullable-receiver rules are a separate topic.) ## Why it matters Knowing extensions are static methods explains their power *and* their limits in one model: they are convenient call syntax over a normal function that takes the receiver as an argument.

  • If a variable's static type is Animal but it holds a Dog, which extension runs?
    The one for the static type (Animal), because extensions dispatch on the declared compile-time type, not the runtime type.
  • How would Java call a Kotlin top-level extension `fun String.shout()` in `Utils.kt`?
    As a static call: `UtilsKt.shout("hi")`, passing the receiver as the first argument.

It's like addressing a letter to a person and handing it to a clerk: the clerk (static method) does the work and the person (receiver) is just the first thing written on the envelope.

saying these in an interview costs you the question

  • Saying extensions are virtual or can be overridden
  • Claiming dispatch uses the runtime type of the receiver
  • Believing the bytecode injects a method into the receiver class
  • Not knowing the receiver becomes the first static parameter

context