Why does `value as List<String>` produce an 'unchecked cast' warning in Kotlin, and what are the consequences at runtime?
answer
- JVM erasure drops generic args at runtime
- as List<String> only verifies List, not String
- CCE is deferred to element read, not the cast line
- List<*> star projection casts cleanly
- reified T in inline fns retains the type
basics
~20 sAt runtime the program does not remember the type inside the list, only that it is a list. So Kotlin can check it is a List but not that it holds Strings. It warns because the cast might wrongly succeed and crash later.
solid answer
~40 sKotlin (on the JVM) uses type erasure: generic type arguments like the String in List<String> are removed at compile time, leaving only List at runtime. So value as List<String> can verify the object is a List, but cannot verify its elements are Strings. The compiler flags this as an 'unchecked cast' warning because the assertion is only partially enforced. The cast succeeds even if the list actually holds Ints; the ClassCastException is deferred until you read an element and assign it to a String variable (a hidden checkcast inserted by the compiler). Mitigations: cast to the erased form List<*> when you only need structural access, use reified type parameters in inline functions where the type is retained, or wrap and validate elements. @Suppress("UNCHECKED_CAST") silences the warning only when you can prove safety.
code
kotlin · 9 linesfun firstOrNull(value: Any): String? {
// structural, no warning:
val list = value as? List<*> ?: return null
// element-level check, no unchecked warning:
return list.firstOrNull() as? String
}
println(firstOrNull(listOf(1, 2))) // null (element isn't String)
println(firstOrNull(listOf("a"))) // "a"go deeper
Recognizes the warning relates to generics not being checked at runtime.
Explains erasure, that only the outer List is verified, and uses List<*> to avoid the warning.
Knows the CCE is deferred to element access and uses reified/filterIsInstance to make checks real.
Sets codebase policy on when suppression is allowed and designs APIs (tokens, sealed types) to avoid erasure-driven casts.
## Type erasure: the root cause On the JVM, Kotlin **generics are erased**: the compiler uses generic types for compile-time checking, but the runtime representation keeps only the **raw** class. `List<String>`, `List<Int>`, and `List<Any?>` are all just `List` at runtime — the `<String>` part is gone. This is called **type erasure**. ## Why the warning appears When you write `value as List<String>`, the compiler emits a JVM `checkcast` that can only confirm `value is List`. It **cannot** confirm the elements are `String`, because that information was erased. Since the cast is only partially verified, the compiler raises an **"unchecked cast"** warning to tell you the runtime check is weaker than the source code implies. ```kotlin fun unsafe(value: Any): Int { @Suppress("UNCHECKED_CAST") val strings = value as List<String> // unchecked: only `is List` is verified return strings.first().length // CCE happens HERE if element isn't a String } val bad: Any = listOf(1, 2, 3) unsafe(bad) // cast 'succeeds', then ClassCastException on .length ``` Notice the failure is **deferred**: the `as` line passes, and the `ClassCastException` only fires when the (Int) element is implicitly cast to `String` at the call site. ## Star projection avoids the warning If you only need structural operations (size, iteration as `Any?`), cast to a **star projection** `List<*>`, which is fully checkable and warning-free: ```kotlin val list = value as? List<*> ?: return println(list.size) // fine val first = list[0] // type is Any? ``` ## Reified type parameters retain the type Inside an **`inline`** function you can mark a type parameter **`reified`**, and the actual type is substituted at the call site so it is available at runtime: ```kotlin inline fun <reified T> Any?.asOrNull(): T? = this as? T val s = (42 as Any).asOrNull<String>() // null, properly checked at the element level for T ``` Reified `T` lets `is`/`as` against `T` be checked — but note that with collections, `list as? List<T>` still only checks the outer `List`, since the element type argument of the *list* is still erased. ## When suppression is acceptable Use `@Suppress("UNCHECKED_CAST")` only when an external invariant guarantees the element type (e.g., you deserialized data you control, or a framework contract). Otherwise validate elements or carry a `KClass`/token.
- At which line does the ClassCastException fire for `(listOf(1) as List<String>).first().length`?Not at the `as` line — that passes because only `is List` is checked. It fires when the Int element is implicitly cast to String for `.length`.
- How can you cast and still get a real per-element guarantee?Cast to `List<*>` and cast each element with `as?`/`is` (e.g., `filterIsInstance<String>()`), which checks elements at runtime.
saying these in an interview costs you the question
- Claiming as List<String> fully checks element types at runtime
- Saying the CCE happens on the cast line (it is deferred)
- Blanket @Suppress("UNCHECKED_CAST") without justifying the invariant
- Confusing erasure with reflection-based retention
- Thinking List<*> is the same as List<Any> for casting purposes