skip to content

Because of JVM type erasure, what does `value as? List<String>` actually check at runtime, and what is the implication for casting generic types safely?

level: seniorimportance: should knowfreq 48%

answer

  1. Erasure: as? List<String> only checks `is List`
  2. Unchecked-cast warning = deferred ClassCastException
  3. Exception surfaces at element USE site, not cast site
  4. Star projection List<*> is fully checkable
  5. reified (inline) + filterIsInstance for element checks

basics

~20 s

On the JVM the generic part is erased, so as? List<String> only checks that the value is some List, not that its elements are Strings. A List<Int> would pass the cast even though the elements are wrong.

solid answer

~40 s

Due to **type erasure**, the JVM keeps no record of a `List`'s element type at runtime. So `value as? List<String>` only verifies that `value` is a `List<*>`; the `<String>` part is unchecked. A `List<Int>` casts successfully, and a `ClassCastException` may surface later only when you read an element and assign it to a `String` variable (a hidden, synthetic cast). The compiler warns 'unchecked cast'. To cast the container only, use a star projection: `value as? List<*>` (no warning, fully checkable). To verify elements you must inspect them at runtime (`all { it is String }`) or rely on `reified` type parameters in inline functions, which preserve the concrete type. Prefer designing with sealed types or validated parsing over casting erased generics.

code

kotlin · 8 lines
kotlin
val value: Any = listOf(1, 2, 3)
val xs = value as? List<String>   // succeeds! unchecked
println(xs != null)               // true
val s: String = xs!![0]           // ClassCastException here

// Safe alternative:
val safe = (value as? List<*>)?.filterIsInstance<String>()
println(safe)                     // []  (no Strings, no crash)

go deeper

for a junior

May not know erasure; can still recall that as? returns null on raw-type mismatch.

for a middle

Knows generic args are erased and recognizes the unchecked-cast warning.

for a senior

Explains deferred ClassCastException at the use site and uses star projection / filterIsInstance correctly.

for a principal

Designs around erasure with reified inline APIs or sealed/typed models, and reasons about where the synthetic checkcast is emitted.

## Type erasure and generic casts **Type erasure** means the JVM discards generic type arguments at compile time. At runtime a `List<String>` and a `List<Int>` are both just `List`. Casts can only check what exists at runtime — the *raw* class — so generic arguments cannot be verified. ### What `as? List<String>` really checks ```kotlin val value: Any = listOf(1, 2, 3) // actually List<Int> val xs = value as? List<String> // WARNING: unchecked cast ``` At runtime the only check performed is `value is List`. Since `listOf(1,2,3)` *is* a `List`, the safe cast **succeeds** and `xs` is non-null — even though every element is an `Int`. The `<String>` is unchecked, hence the compiler's **'unchecked cast'** warning. The mismatch is deferred: ```kotlin val first: String = xs!![0] // ClassCastException HERE (synthetic cast Int->String) ``` The exception appears at the *use site*, not the cast site — a debugging trap. ### Safe ways to deal with it **1. Star projection** — cast only the container, which is fully checkable: ```kotlin val xs = value as? List<*> // no warning; xs: List<*>? ``` Elements are typed `Any?`; you handle each explicitly. **2. Runtime element inspection:** ```kotlin fun asStringList(value: Any?): List<String>? = (value as? List<*>)?.takeIf { list -> list.all { it is String } } ?.filterIsInstance<String>() ``` `filterIsInstance<T>()` itself uses a `reified` parameter to check each element. **3. `reified` type parameters** in `inline` functions preserve the concrete type so element-level checks become possible: ```kotlin inline fun <reified T> Any?.asListOf(): List<T>? = (this as? List<*>)?.takeIf { it.all { e -> e is T } } ?.map { it as T } ``` `reified` works only because `inline` substitutes the real type into the bytecode; it requires `inline`. ### Key takeaways - `as?`/`as`/`is` on generics check only the **raw** type; arguments are erased. - An erased generic cast that 'succeeds' can still blow up later at the element use site. - Use `List<*>` (star projection) for warning-free container checks, `filterIsInstance`/`reified` for element checks. - Treat pervasive generic casting as a design smell — prefer typed parsing or sealed models.

  • Why does the ClassCastException appear when reading an element rather than at the cast?
    The cast only checks the raw `List` type (erasure), so it passes. The compiler inserts a synthetic checkcast when an element is assigned to a typed variable, and that synthetic cast fails at the use site.
  • How does `filterIsInstance<String>()` avoid the erasure problem?
    It is an inline function with a `reified` type parameter, so the concrete type is available in bytecode and each element is checked with `is` at runtime.

It's like checking a sealed box is labeled 'CRATE' but never opening it — you only learn the contents are wrong when you reach in and grab one.

saying these in an interview costs you the question

  • Claiming `as? List<String>` checks element types at runtime
  • Not knowing what type erasure is
  • Ignoring the 'unchecked cast' compiler warning
  • Thinking the cast itself throws for wrong element types
  • Believing `reified` works without `inline`

context