skip to content

Reification & Erasure

The JVM forgets generic type arguments at runtime, and Kotlin's reified type parameters buy some of that information back for inline functions. This pair of topics explains most 'why can't I do that with generics' questions.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

explore

questions

20

What is JVM type erasure, and what happens to the type argument of a `List<String>` at runtime in Kotlin?

level: juniorimportance: must knowfreq 70%

answer

  1. Type args are compile-time only
  2. Runtime sees raw List = List<*>
  3. Compiler adds hidden CHECKCAST
  4. is List<String> won't compile
  5. Same javaClass for List<String> and List<Int>

basics

~10 s

At compile time the type knows it holds Strings, but at runtime the JVM forgets the generic part. A List<String> and a List<Int> are just a plain List to the running program.

solid answer

~40 s

Type erasure means generic type arguments exist only at compile time; the JVM removes them before running. The compiler uses `<String>` to type-check your code and insert hidden casts, but at runtime `List<String>` and `List<Int>` are both represented by the same raw `List` class — Kotlin writes this as `List<*>` (a star-projected list). So you cannot ask, at runtime, what element type a list holds: there is no metadata for it on the instance. This is why `value is List<String>` does not compile in Kotlin: the runtime can only check `value is List<*>`. Reflection and a few container types keep some generic info in class metadata, but ordinary instances do not carry their type arguments.

code

kotlin · 8 lines
kotlin
val a: List<String> = listOf("hi")
val b: List<Int> = listOf(1)
println(a.javaClass == b.javaClass) // true

fun describe(x: Any) {
    if (x is List<*>) println("some list, size=${x.size}")
    // if (x is List<String>) ... // does not compile
}

go deeper

for a junior

Can state that generics are compile-time only and the runtime sees a raw list; knows is List<String> is rejected.

for a middle

Explains the compiler-inserted casts and why List<String>/List<Int> share a class; uses List<*> correctly.

for a senior

Connects erasure to overload clashes, unchecked-cast warnings, and what reflection still preserves.

for a principal

Frames erasure as a JVM compatibility decision, contrasts with reified-generics platforms, and reasons about API design under erasure.

## What erasure is **Generics** let you parameterize a type, like `List<String>` (a list whose elements are `String`). **Type erasure** is the JVM rule that these type arguments (`<String>`, `<Int>`) are a *compile-time-only* feature: the compiler uses them to check your code, then **erases** them so the bytecode contains only the raw class `List`. Kotlin, like Java, targets the JVM and inherits this. The benefit historically was backward compatibility (generics were added to Java 5 without changing the JVM); the cost is that an object does **not** know its own type arguments at runtime. ## What the compiler actually does - It type-checks: it rejects `list.add(42)` on a `List<String>`. - It inserts **hidden casts** so that when you read an element as `String`, a `CHECKCAST` to `String` is emitted automatically. - It then throws the `<String>` away. The class file just references `java.util.List`. ## Why `List<String>` is `List<*>` at runtime Because the element type is gone, the only honest runtime view is **`List<*>`** — a *star projection* meaning "a List of some unknown type." You can check `x is List<*>` ("is this a list at all?") but not `x is List<String>`. ```kotlin val a: List<String> = listOf("hi") val b: List<Int> = listOf(1) println(a.javaClass == b.javaClass) // true: both are the same erased class fun describe(x: Any) { if (x is List<*>) println("a list of ${x.size} unknown-typed items") // if (x is List<String>) ... // compile error: cannot check erased type } ``` ## Consequences you'll meet - `is List<String>` / `as List<String>` (the cast version warns as an **unchecked cast**) cannot be verified at runtime. - You cannot overload two functions that differ only by a type argument (`f(List<String>)` vs `f(List<Int>)`) — after erasure they have the same JVM signature. - A plain type parameter `T` inside a normal function has no runtime identity; you cannot write `T::class` or `x is T`. ## What is *not* erased - The **raw class** survives: `"x".javaClass` is `String`. - Generic info in **declarations** (field/method/superclass signatures) is kept in metadata and is readable via **reflection** (`KType`, `java.lang.reflect.Type`), which is how serializers recover element types. - Kotlin offers **`reified`** type parameters on `inline` functions to dodge erasure at the call site (covered in a sibling topic).

  • If type arguments are erased, how does `list[0]` still return a `String` and not `Any`?
    The compiler inserts an automatic `CHECKCAST` to `String` at the read site, based on the static type it tracked at compile time. The runtime cast is real; the generic source-level info that justified it is gone.
  • Does erasure mean Kotlin generics are useless at runtime?
    No. They give full compile-time type safety and eliminate manual casts. Only runtime *introspection* of the argument is lost — and even that is partially recoverable via reflection or `reified`.

Erasure is like shipping a labeled box: the label (<String>) guides the packer, but it's peeled off before delivery, so the receiver only sees a generic box.

saying these in an interview costs you the question

  • Claiming `List<String>` keeps its element type on the instance at runtime
  • Saying `x is List<String>` compiles and works
  • Confusing erasure with boxing/autoboxing
  • Thinking Kotlin avoids erasure entirely because it's not Java
  • Asserting `List<String>` and `List<Int>` are different classes at runtime

context

open as a page

The JVM erases generics at runtime, so a method that takes a List<T> cannot 'see' what T is. How do you pass the concrete type into a non-inline function so it survives to runtime, and what is the difference between Class<T> and KClass<T>?

level: juniorimportance: must knowfreq 60%

basics

~20 s

You hand the type in by hand as a parameter, like a label. Add a Class<T> or KClass<T> argument so the function knows the real type at runtime. Class is the Java type token; KClass is the Kotlin one.

open as a page

Why must a function be marked `inline` for its type parameter to be `reified`? What does the compiler do?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Because inline copies the function's body into each call site. There the compiler already knows the real type you used, so it can replace the type parameter with that concrete class. A normal function has no copy and no known type.

open as a page

What does it mean to declare a function with a `reified` type parameter in Kotlin, and what does it let you do that a normal generic function cannot?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Marking a generic type with reified (on an inline function) keeps the real type available at runtime. So you can write value is T or T::class, which you normally cannot do because generics are erased.

open as a page

Why does `if (value is List<String>)` fail to compile in Kotlin, while `if (value is List<*>)` is allowed?

level: middleimportance: must knowfreq 62%

basics

~20 s

At runtime the program can't tell what's inside a list, only that it's a list. So checking is List<String> is impossible and Kotlin rejects it; is List<*> (a list of anything) is checkable, so it's allowed.

open as a page

Why can't you put `reified` on the type parameter of a regular class, a constructor, or a non-inline function, and what is the standard workaround when you genuinely need the type inside one of those?

level: middleimportance: must knowfreq 50%

basics

~20 s

Reified only works on inline functions because the compiler copies the function body and bakes the real type in. Classes and normal functions exist once at runtime, so there's nothing to bake into. The fix: pass a Class<T>/KClass<T> token instead.

open as a page

Show how the inline+reified pattern turns Gson's `fromJson(json, Class)` into `fromJson<T>(json)`. What gets generated at the call site?

level: middleimportance: must knowfreq 65%

basics

~20 s

You write a small inline extension with a reified type. Inside it you can use T::class.java, so you call the old fromJson(json, T::class.java) for the caller. At the call site T is replaced by the real class, so the user just writes fromJson<User>(json).

open as a page

Why won't this compile, and what does the error tell you about erasure? ```kotlin fun process(items: List<String>) {} fun process(items: List<Int>) {} ```

level: middleimportance: should knowfreq 45%

basics

~10 s

After the generic part is stripped away, both functions look identical to the JVM — both take a plain List. You can't have two functions that look the same, so it fails to compile.

open as a page

A KClass<T> only captures the raw class, so passing Map::class loses the key/value types. How do you capture a FULL generic type such as Map<String, List<User>> at runtime in Kotlin, and what does typeOf() return?

level: middleimportance: should knowfreq 45%

basics

~10 s

A class token forgets the inside types. To keep the whole shape like Map<String, List<User>>, use typeOf<...>() (or a library TypeReference). It gives you a full KType describing every nested type argument.

open as a page

Inside `inline fun <reified T> a()` you call another generic helper `b<T>()`. What must be true of `b` for this to compile, and why?

level: middleimportance: should knowfreq 35%

basics

~20 s

If b also needs the real type (it's reified), then b must itself be an inline reified function. A reified type can only be passed to another reified parameter, because only there is the concrete class still known.

open as a page

How does the standard-library function `filterIsInstance<T>()` work, and why must it be `inline` with a `reified` parameter? Show how it differs from a manual `filter { it is T }`.

level: middleimportance: should knowfreq 40%

basics

~20 s

filterIsInstance<T>() keeps only elements that are of type T and returns them already typed as T. It must be inline + reified so the is T check works at runtime; a plain filter { it is T } can't even compile because T would be erased.

open as a page

Inside a reified function, what is the difference between `T::class` and `typeOf<T>()`? When would you need `typeOf` instead of `T::class`?

level: middleimportance: should knowfreq 45%

basics

~20 s

T::class gives the class only and drops generic arguments — List<String> becomes just List. typeOf<T>() gives the full type including arguments, so it can tell List<String> from List<Int>. Use typeOf when the generic arguments matter.

open as a page

Given erasure removes type arguments from instances, how can a JSON library still figure out that a field is a `List<String>` versus a `List<Int>` at runtime?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Instances forget their type, but the declarations (fields, method signatures) keep the generic info in class metadata. Libraries read that metadata with reflection to learn the element type — they don't ask the object itself.

open as a page

Inside a generic function `fun <T> makeBuffer(size: Int)`, why can't you just write `Array<T>(size) { ... }` to create a generic array, and what are the idiomatic Kotlin ways around this limitation?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Arrays need to know their exact element type at runtime, but T is erased, so the runtime has no real type to build. Workarounds: use a reified inline function, pass a Class<T> token, build an Array<Any?> and cast, or just use a List/ArrayList.

open as a page

An inline function with a reified parameter also takes a lambda. How do `noinline`/`crossinline` and the inlining contract interact with reified, and what constrains where you can call such a function?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Reified needs the whole function inlined. Lambda parameters are inlined too by default; marking one noinline keeps that lambda as a real object but doesn't break reified, since the function body is still copied. crossinline forbids non-local returns from the lambda.

open as a page

Reified type parameters cannot do everything. Name concrete limitations of `reified` and explain how you work around them (e.g., needing a real `T` instance or the element type of `T`).

level: seniorimportance: should knowfreq 30%

basics

~20 s

Reified can't create a T (no T()), can't be used outside inline functions or in classes, and still can't see a type's own generic arguments at runtime. Work around it with a factory lambda, a passed Class<T>/KClass<T>, or typeOf<T>() for nested generics.

open as a page

Contrast `List<*>`, `List<Any?>`, and a raw `List` (as seen from Java). What does each mean and how do they relate to erasure?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

List<*> is a list of some unknown type you can read from but not safely add to. List<Any?> is a list explicitly of anything. A raw List is the untyped Java view that erasure leaves behind, which Kotlin discourages.

open as a page

You're designing a generic deserialization API that must support nested generics like List<Map<String, User>> and stay performant. Compare the runtime type-information options (KClass token, KType via typeOf, super type token / TypeReference, reified wrapper) and how you'd combine them without forcing reflection costs on hot paths.

level: principalimportance: nice to knowfreq 20%

basics

~20 s

A plain class token can't hold nested generics, so you need a full type model: KType (from typeOf) or a TypeReference. Offer a friendly reified entry point that builds it once, then cache the heavy reflection result so repeated calls are cheap.

open as a page

You expose a widely-used `inline fun <reified T> decode(bytes: ByteArray): T` in a public library. What are the bytecode/ABI and binary-compatibility consequences, and how would you mitigate them?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Because it's inline, the body is copied into every place callers use it, so the real logic lands in their code. Changing the body forces recompilation and can bloat their binaries. Keep the inline part tiny and delegate the heavy work to a normal function.

open as a page

Explain precisely how the compiler implements a `reified` parameter at the bytecode level, and what consequences this has for binary compatibility and public library APIs.

level: principalimportance: nice to knowfreq 18%

basics

~20 s

There is no real generic function at runtime. The compiler copies the function body into each call site and replaces T with the concrete type there. So the type info lives in the caller's bytecode, which affects inlining cost, binary compatibility, and what callers (e.g. Java) can use.

open as a page