What is JVM type erasure, and what happens to the type argument of a `List<String>` at runtime in Kotlin?
answer
- Type args are compile-time only
- Runtime sees raw List = List<*>
- Compiler adds hidden CHECKCAST
- is List<String> won't compile
- Same javaClass for List<String> and List<Int>
basics
~10 sAt 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 sType 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 linesval 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
Can state that generics are compile-time only and the runtime sees a raw list; knows is List<String> is rejected.
Explains the compiler-inserted casts and why List<String>/List<Int> share a class; uses List<*> correctly.
Connects erasure to overload clashes, unchecked-cast warnings, and what reflection still preserves.
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