skip to content

JVM Type Erasure

At runtime a List<String> is just a List, which is why you cannot ask whether a value is a List<String>. Interviewers expect you to name erasure as the cause and to know the workarounds rather than just calling it a limitation.

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

questions

5

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

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 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

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

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