skip to content

In Kotlin, when you call an extension function, what determines which function actually runs: the variable's declared type or the actual object stored in it?

level: juniorimportance: must knowfreq 70%

answer

  1. Extensions resolve statically
  2. Declared type, not runtime object
  3. Members = virtual dispatch
  4. Compiles to static fn with receiver param
  5. No vtable entry for extensions

basics

~10 s

The variable's declared type decides. Kotlin picks the extension function at compile time based on how the variable is typed, not on the real object it holds at runtime.

solid answer

~40 s

Extension functions are resolved statically. The compiler chooses which extension to call from the static (declared/compile-time) type of the receiver expression, not from the object's runtime class. This is the opposite of how regular open member methods behave, which use virtual dispatch and look at the actual runtime object. So if you store a `Dog` in a variable typed as `Animal`, an extension defined on `Animal` will run even though the object is really a `Dog`. Extensions don't modify the class and aren't part of its vtable; they compile down to plain static functions that take the receiver as a hidden first parameter. Practical rule: extension calls follow the type you wrote, member calls follow the object you have.

code

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

fun Animal.speak() = "generic noise"
fun Dog.speak() = "woof"

fun main() {
    val pet: Animal = Dog()
    println(pet.speak()) // "generic noise" — chosen by declared type Animal
    val dog: Dog = Dog()
    println(dog.speak()) // "woof" — declared type is Dog
}

go deeper

for a junior

Knows the call follows the declared type and can read the simple Dog/Animal example correctly.

for a middle

Explains it compiles to a static function and contrasts with virtual member dispatch.

for a senior

Articulates why (no vtable entry), ties it to design risks, and predicts the result under type inference.

for a principal

Frames it as a deliberate language design tradeoff and warns teams about subtype surprises in shared APIs.

## The core idea Kotlin **extension functions** let you call a function with dot syntax (`obj.foo()`) on a type you don't own, without editing that type. But they do **not** add anything to the class. The compiler turns an extension into an ordinary static (top-level) function whose first hidden parameter is the **receiver**. ## Static vs dynamic resolution - **Static (compile-time) resolution**: the compiler decides which function to call by looking at the **declared type** of the expression you wrote — the type you can see in the source. - **Dynamic (virtual) dispatch**: at runtime the JVM looks at the **actual class** of the object and calls the most-derived override. **Member functions** use virtual dispatch (when `open`/overridden). **Extension functions** use static resolution. ```kotlin open class Animal class Dog : Animal() fun Animal.speak() = "generic noise" fun Dog.speak() = "woof" val pet: Animal = Dog() println(pet.speak()) // prints "generic noise" ``` Even though `pet` holds a `Dog`, the **declared type** is `Animal`, so `Animal.speak()` is chosen at compile time. ## Why this happens The extension above compiles roughly to: ```kotlin fun speak(receiver: Animal): String = "generic noise" ``` A static call like `speak(pet)` is bound using the static type of `pet`. There is no vtable entry, no runtime lookup. ## The key contrast | | Member function (open) | Extension function | |---|---|---| | Resolved | At runtime | At compile time | | Based on | Actual object class | Declared receiver type | | Overridable | Yes | No (only shadowed) | Remember the terms: **receiver** = the object before the dot; **declared/static type** = the type written in source; **runtime type** = the real class. Extensions follow the declared type.

  • If you change `val pet: Animal = Dog()` to `val pet = Dog()`, what does `pet.speak()` print and why?
    "woof". Type inference makes the declared type `Dog`, so the compiler statically selects `Dog.speak()`.

An extension is like a label you read off the box (the declared type), not what's actually inside the box (the runtime object).

saying these in an interview costs you the question

  • Saying extensions use polymorphism/virtual dispatch like overridden methods
  • Claiming the runtime object decides which extension runs
  • Believing extensions are added to the class or vtable
  • Confusing extensions with overriding

context