skip to content

When would you deliberately use java.lang.reflect instead of kotlin-reflect in a Kotlin codebase, and how do you bridge between the two?

level: principalimportance: should knowfreq 28%

answer

  1. kotlin-reflect ~3MB + first-use metadata parse
  2. Java reflect: zero-dep, JVM model
  3. Bridges: .java/.kotlin, javaMethod/kotlinFunction
  4. Native image / Android favor lean Java reflect
  5. Introspect with Kotlin, cache Java handles for hot paths

basics

~10 s

Use Java reflection when you don't need Kotlin-specific info, want to avoid the extra kotlin-reflect dependency and startup cost, or interop with Java frameworks. Bridge with .java and .kotlin and the jvm extension functions.

solid answer

~40 s

kotlin-reflect is a separate ~3MB artifact that loads and decodes @Metadata, which adds binary size and first-use startup cost. So you drop to java.lang.reflect when: you only need class/method/field/annotation data the JVM already exposes; you're on a startup- or size-sensitive target (Android, serverless, GraalVM native image where reflection must be registered); or you're feeding a Java framework that speaks Class/Method. Bridges: `KClass.java` and `Class.kotlin`; from kotlin.reflect.jvm: `KFunction.javaMethod`, `KFunction.javaConstructor`, `KProperty.javaField`/`javaGetter`, and reverse `Method.kotlinFunction`, `Field.kotlinProperty`. Conversely use kotlin-reflect when you need nullability, default args (callBy), properties, suspend detection, KType/typeOf, or variance. A common pattern: introspect with kotlin-reflect once, cache resolved java.lang.reflect handles for hot-path invocation.

code

kotlin · 9 lines
kotlin
import kotlin.reflect.jvm.javaMethod
import kotlin.reflect.full.functions

class Svc { fun ping() = "pong" }

// Introspect with kotlin-reflect, then cache the java.lang.reflect Method
val kFn = Svc::class.functions.first { it.name == "ping" }
val method = kFn.javaMethod!!            // bridge KFunction -> Method
method.invoke(Svc())                     // fast invoke on hot path

go deeper

for a junior

Knows the two reflection APIs coexist and you can convert with .java and .kotlin.

for a middle

Lists cases (no Kotlin info needed, Java framework interop) and names the main bridge functions.

for a senior

Weighs dependency size and first-use cost and uses the introspect-then-cache hybrid pattern.

for a principal

Sets codebase policy on reflection use across Android/native/serverless targets, balancing fidelity, startup, and binary size.

## The trade-off in one line kotlin-reflect = richer, Kotlin-faithful model **at a cost**; java.lang.reflect = leaner, JVM-only model that's always present. ## Why java.lang.reflect is sometimes the right call - **Dependency & size**: `kotlin-reflect` is a separate artifact (~3MB) you must add explicitly. `java.lang.reflect` is in the JDK — zero dependency. - **Startup / first-use cost**: kotlin-reflect lazily parses the `@Metadata` blob the first time you touch the rich API. On Android, serverless cold starts, or CLI tools this can matter. - **GraalVM native image**: all reflection needs registration, but kotlin-reflect's metadata machinery adds more to register and reason about; minimizing it eases native builds. - **Java interop**: Spring, Jackson (without the Kotlin module), JPA, etc. operate on `Class`/`Method`/`Field`. If you're plugging into them, Java handles are the currency. - **You simply don't need Kotlin semantics**: enumerating annotations, calling a no-default method, reading a `@JvmField` — Java reflection suffices. ## Why kotlin-reflect when you DO need it Nullability (`KType.isMarkedNullable`), default args (`callBy`/`isOptional`), unified properties (`memberProperties`), suspend detection (`isSuspend`), generics that survive (`typeOf<T>()`, `KType`, variance), Kotlin visibility/modality (`KVisibility`, `isOpen`). None of these are visible to Java reflection. ## Bridging between the two ```kotlin import kotlin.reflect.jvm.* val kClass = String::class val jClass: Class<String> = kClass.java // KClass -> Class val back = jClass.kotlin // Class -> KClass fun example(fn: kotlin.reflect.KFunction<*>) { val m = fn.javaMethod // KFunction -> Method? val ctor = fn.javaConstructor // KFunction -> Constructor? } // reverse direction // method.kotlinFunction, field.kotlinProperty ``` Property bridges: `KProperty.javaField`, `KProperty.javaGetter`, `KMutableProperty.javaSetter`. ## A pragmatic hybrid pattern 1. **Introspect** the model once with kotlin-reflect (nullability, defaults, property list) at startup. 2. **Resolve** to `java.lang.reflect` handles (`Method`, `Field`) and **cache** them. 3. **Invoke** on hot paths through the cached Java handles (or generated invokers), which avoids per-call kotlin-reflect overhead. This gives you Kotlin-faithful configuration with Java-speed execution. ## Gotchas - `.java`/`.kotlin` round-trips are cheap, but `javaMethod` can be `null` (e.g., for fake-override or intrinsic members). - `setAccessible`/`isAccessible` is governed by JPMS module openness regardless of which reflection API you use. - Mixing the two means you own correctness around name mangling and synthetic members on the Java side.

  • Name two concrete reasons to avoid kotlin-reflect on Android or in a native image.
    It's an extra ~3MB dependency that increases app size, and its @Metadata parsing adds first-use startup cost; native images also require registering all that reflective surface.
  • How do you go from a KClass to a java.lang.Class and back?
    kClass.java gives the Class; clazz.kotlin gives the KClass. They model the same runtime type from the two reflection systems.

saying these in an interview costs you the question

  • Always reaching for kotlin-reflect even when Java reflection suffices
  • Unaware kotlin-reflect is a separate dependency with startup cost
  • Not knowing the .java/.kotlin and javaMethod/kotlinFunction bridges
  • Ignoring native-image/Android constraints when choosing

context