You must safely cast an erased `Any` (e.g., a deserialized value) into a generic type like `List<User>`. How do you make this robust given type erasure, and when is `@Suppress("UNCHECKED_CAST")` justified?
answer
- erasure: only List is checked, not User
- filterIsInstance<User> = checked per element
- type token (KClass/TypeReference/typeOf) avoids the cast
- reified removes outer warning, element cast still guards
- suppress only with an external invariant + test + comment
basics
~20 sBecause the program forgets the inside type of a list at runtime, you cannot fully trust one big cast. Check each element instead, or use a tool that keeps the type, and only silence the warning when something else guarantees the contents are correct.
solid answer
~40 sType erasure means as List<User> only verifies List, so a single cast cannot guarantee elements. Robust options, in order of preference: (1) filterIsInstance<User>() to checked-filter elements; (2) validate each element with as?/is while building the typed list; (3) use a reified inline helper for the outer type and per-element checks; (4) carry a runtime token (KClass<User> or a TypeReference / Jackson TypeFactory) so the deserializer constructs the right type up front. @Suppress("UNCHECKED_CAST") is justified only when an external invariant proves the element type — e.g., a serializer you control wrote it, or a sealed/closed contract guarantees it — and you should localize the suppression to the smallest scope, comment the invariant, and ideally assert it in a test. Otherwise a deferred ClassCastException will surface far from the cast.
code
kotlin · 9 lines// Generic cache where suppression is justified by the insert contract
class TypedCache {
private val map = mutableMapOf<Class<*>, Any>()
fun <T : Any> put(type: Class<T>, value: T) { map[type] = value }
fun <T : Any> get(type: Class<T>): T? {
@Suppress("UNCHECKED_CAST") // safe: only put(type, T) ever inserts under `type`
return map[type] as T?
}
}go deeper
Understands you can't fully cast to List<User> at runtime and should check elements.
Uses filterIsInstance / per-element as? to build a typed list safely.
Adds reified helpers and type tokens, and explains the deferred-CCE failure mode.
Sets the codebase policy for when suppression is allowed and designs token-based APIs that avoid erased casts entirely, with tests guarding the invariants.
## The constraint: erasure hides element types On the JVM, `List<User>` erases to `List`. A cast `value as List<User>` emits a `checkcast List` and **nothing** for the `User` argument. So the cast can succeed over a list of the wrong elements, and a `ClassCastException` only fires later when an element is implicitly cast to `User`. Any "robust" approach must therefore check **elements**, not just the container. ## Robust strategies (preferred first) **1. `filterIsInstance` — checked and lossless of intent:** ```kotlin fun usersOf(value: Any): List<User> = (value as? List<*>)?.filterIsInstance<User>() ?: emptyList() ``` Every element is `is User`-checked; non-matching elements are dropped rather than crashing later. Use when silently skipping bad elements is acceptable. **2. Validate-and-fail explicitly** when a wrong element should be an error: ```kotlin fun usersStrict(value: Any): List<User> { val raw = value as? List<*> ?: error("not a list") return raw.map { it as? User ?: error("bad element: $it") } } ``` **3. Reified helper** retains the *outer* type for inline call sites: ```kotlin inline fun <reified T> Any.asListOf(): List<T> = (this as? List<*>)?.map { it as T } ?: emptyList() ``` Still relies on per-element `as T`; `reified` removes the outer unchecked warning but element casts remain the real guard. **4. Carry a runtime token** so deserialization builds the correct type and casting is unnecessary: ```kotlin // Jackson example val users: List<User> = mapper.readValue(json, object : TypeReference<List<User>>() {}) ``` A `KClass<T>`, `TypeReference`, or `KType` (from `typeOf<List<User>>()`) preserves the full generic shape at runtime, sidestepping erasure entirely. This is the most robust design when you own the deserializer. ## When `@Suppress("UNCHECKED_CAST")` is justified Suppression is acceptable only when an **invariant outside the type system** proves the element type, such as: - A serializer/format **you control** wrote homogeneous data. - A sealed/closed contract or builder guarantees membership. - A generic cache keyed by `KClass<T>` whose insertion path enforces the type. Discipline when you suppress: - Annotate the **smallest** scope (local val, not the function). - Add a comment stating the invariant that makes it safe. - Back it with a test that would fail if the invariant breaks. ## Design takeaway Prefer designs that never need the cast: reified APIs, type tokens, and per-element validation move the check to where the type information actually exists, turning a deferred, far-away `ClassCastException` into an immediate, local, well-located failure.
- Why is `filterIsInstance<User>()` safer than `as List<User>`?It runs an `is User` check on every element, so the result genuinely contains only Users; the bare cast checks nothing about elements and defers the CCE.
- What does a type token (e.g., typeOf<List<User>>() / TypeReference) buy you?It preserves the full generic type at runtime, letting a deserializer build the correctly typed structure so no erased cast is needed at all.
- How would you keep a justified suppression honest over time?Localize it to the smallest scope, comment the proving invariant, and add a unit test that fails if the invariant (e.g., the only insert path) is ever violated.
saying these in an interview costs you the question
- Suppressing UNCHECKED_CAST with no stated invariant or test
- Believing reified makes a List element cast fully checked
- Casting the whole list instead of validating elements
- Suppressing at function scope rather than the minimal expression
- Not recognizing type tokens as the way to defeat erasure cleanly