How does star projection interact with runtime type checks (`is`/`as`) and generic erasure? Why does `x is List<*>` compile while `x is List<String>` does not?
answer
- JVM erases type arguments at runtime
- Only `is List<*>` allowed; `is List<String>` won't compile
- Star every parameter in an `is` check
- `as List<String>` -> unchecked cast, delayed CCE
- reified + inline (filterIsInstance) to check elements
basics
~20 sKotlin erases generic type arguments at runtime, so you can't check is List<String> — the element type isn't there to check. The star projection is List<*> only checks that it's a List at all, which is exactly what's verifiable after erasure.
solid answer
~40 sJVM generics are **erased**: at runtime a `List<String>` and a `List<Int>` are both just `List`. So `x is List<String>` is rejected with 'cannot check for instance of erased type.' The **only** type-argument shape allowed in an `is`/`as` check is the star projection: `x is List<*>` succeeds because it checks solely that `x` is some `List`, ignoring the erased element type. After a successful `x is List<*>`, the smart-cast type is `List<*>`, so reads are `Any?`. `as List<String>` compiles but triggers an *unchecked cast* warning and gives no runtime guarantee — a `ClassCastException` may surface later when you read an element. Use `reified` type parameters in `inline` functions (e.g. `filterIsInstance<String>()`) when you need to check concrete element types.
code
kotlin · 13 linesfun summarize(x: Any): String = when (x) {
is List<*> -> "list of ${x.size}" // only star form allowed
is Map<*, *> -> "map of ${x.size}" // star every parameter
else -> "other"
}
inline fun <reified T> Iterable<*>.onlyOf(): List<T> =
filterIsInstance<T>() // reified makes per-element checks possible
fun main() {
println(summarize(listOf(1, 2, 3))) // list of 3
println(listOf(1, "a", 2).onlyOf<Int>()) // [1, 2]
}go deeper
Knows generics are erased and that is List<*> is the allowed form.
Explains the compile error for is List<String> and the unchecked-cast warning for as.
Uses reified/filterIsInstance correctly and reasons about delayed ClassCastException.
Discusses erasure trade-offs, when unchecked casts are justified, and array reification differences.
## Erasure: the root cause On the JVM, generic **type arguments are erased** at runtime. `List<String>`, `List<Int>`, and `List<*>` all share the same runtime class. The compiler therefore can't emit a check that distinguishes element types. ## What you can and cannot write ```kotlin fun check(x: Any) { if (x is List<*>) { // OK: only checks 'is it a List?' val first: Any? = x.firstOrNull() } // if (x is List<String>) {} // COMPILE ERROR: cannot check erased type } ``` - **`x is List<*>`** is allowed because it asks only "is this a `List` of *something*?" — answerable after erasure. - **`x is List<String>`** is a compile error: *"Cannot check for instance of erased type."* - The star is the **only** permitted argument form in `is`. You must star *every* parameter: `x is Map<*, *>`, not `Map<String, *>`, in an `is` check. ## Smart-cast result After `x is List<*>` the compiler smart-casts `x` to `List<*>`, so element reads are `Any?` and writes (if it were mutable) are `in Nothing`. ## Unchecked casts ```kotlin @Suppress("UNCHECKED_CAST") val strings = anyList as List<String> // unchecked: no runtime verification ``` `as List<String>` compiles but produces an **unchecked cast** warning. The cast only verifies the object is a `List`; the `String` part is unverifiable, so a wrong element type fails *later* with `ClassCastException` at the point of use (a 'delayed' CCE). Treat unchecked casts as assertions you must justify. ## reified to recover element checks For checking *element* types, use an `inline` function with a `reified` type parameter: ```kotlin inline fun <reified T> Iterable<*>.onlyOf(): List<T> = filterIsInstance<T>() // reified keeps T at runtime; checks each element ``` `reified T` keeps `T` available at runtime so per-element `it is T` works — but it checks each element, it does *not* let you check `List<T>` as a whole. ## Arrays differ Arrays are reified on the JVM (`Array<String>` keeps its component type), unlike erased generics — but for the generic `is` check the rule stands: only `*` is allowed. ## Practical guidance - Use `is List<*>` to branch on container shape. - Use `filterIsInstance<String>()` (a stdlib reified helper) instead of unchecked casts when you need typed elements. - Suppress `UNCHECKED_CAST` only with a clear invariant comment.
- Why does `as List<String>` compile but `is List<String>` doesn't?`as` is allowed as an *unchecked* cast (compiler warns and skips element verification, risking a later `ClassCastException`), whereas `is` must give a definite runtime answer, which erasure makes impossible for a concrete element type — so it's a hard error.
- How would you safely extract only the `String` elements from a `List<*>`?Use the stdlib `filterIsInstance<String>()`, an inline reified helper that checks each element with `it is String` at runtime — no unchecked cast needed.
Erasure is like shipping boxes with the contents-label torn off: you can confirm it's a box (is List<*>) but not what's inside (is List<String>) without opening each item (a reified per-element check).
saying these in an interview costs you the question
- Claiming `is List<String>` works at runtime
- Thinking `as List<String>` actually verifies element types
- Not knowing every parameter must be starred in an `is` check
- Confusing erased generics with reified arrays/`reified` params
- Suppressing UNCHECKED_CAST without any justifying invariant