In a hot deserialization path you see heavy use of `as?` with Elvis defaults. How do you reason about correctness and cost, and when would you redesign away from runtime casting?
answer
- as? ~ instanceof + null branch: cheap, profile first
- as? ?: default can MASK bad input — fail loudly when needed
- Pervasive as? = lost type info; parse to typed model once
- Sealed types + exhaustive when => cast-free core
- Erasure makes generic as? unsafe in hot paths
basics
~20 sas? is a cheap type check, so cost is rarely the issue. The real concern is design: lots of as? means types are uncertain everywhere. Push validation to the boundary and use typed models so the core code never casts.
solid answer
~50 sCorrectness first: `as?` returns nullable, so every `as?` paired with `?:` silently swaps a fallback on mismatch — that can mask real data errors. Performance: an `as?` compiles to an `instanceof`-style check plus a null branch; it's effectively free relative to JSON parsing, so micro-optimizing it is misguided — measure before assuming. The design smell is *pervasive* casting: it indicates type information was lost (everything is `Any`). The fix is to validate and convert once at the boundary into a precise model — a `sealed class` hierarchy, data classes, or a typed parser (kotlinx.serialization) — so downstream code is statically typed and needs no casts. Reserve `as?` for genuine boundary ambiguity (heterogeneous external payloads). Watch erasure on generic casts, and prefer `when (x) { is A -> ... }` over chains of `as?` for multi-type dispatch.
code
kotlin · 13 lines// Smell: cast-and-default deep in logic, mismatch hidden
val timeout = config["timeout"] as? Long ?: 30L
// Better: parse once at the boundary into a typed model
sealed interface Setting
data class Timeout(val seconds: Long) : Setting
fun parseTimeout(v: Any?): Timeout = when (v) {
is Long -> Timeout(v)
is Int -> Timeout(v.toLong())
else -> error("timeout must be numeric, was $v")
}
// downstream uses Timeout.seconds — no casts, fully typedgo deeper
Knows the as?/Elvis mechanics but not the design/correctness implications.
Sees that as? ?: default hides mismatches and that casts cluster where types are uncertain.
Argues cost is negligible vs parsing and starts pushing casts to the boundary with typed models.
Drives a parse-at-the-boundary architecture with sealed/typed models, distinguishes default-vs-error semantics, and reasons about erasure and profiling rigorously.
## Reasoning about `as?` in a hot path ### Cost model An `as? T` compiles to roughly an `instanceof` test followed by a conditional that yields the value or `null`. On the JVM this is a cheap, well-predicted branch. In a deserialization path dominated by string parsing, allocation, and reflection, the cast is **noise** in the profile. So the first principle: **measure, don't guess** — optimizing `as?` for speed is almost always premature. ### Correctness is the real risk The `as? T ?: default` idiom *swallows* type mismatches: ```kotlin val port = config["port"] as? Int ?: 8080 ``` If `config["port"]` is the *string* `"7000"`, the cast fails and you silently get `8080` — a misconfiguration masquerading as a default. In a boundary parser this can hide malformed input. Decide deliberately whether a mismatch is 'use default' or 'reject input'; for the latter, fail loudly instead: ```kotlin val port = (config["port"] as? Int) ?: error("port must be an Int, was ${config["port"]}") ``` ### The design smell: pervasive casting Many `as?` calls deep in business logic signal that **type information was erased upstream** — typically everything became `Any`/`Map<String, Any?>`. The remedy is to **parse, don't validate-then-cast**: convert untyped input into a precise typed model exactly once at the boundary. ```kotlin sealed interface Event data class Click(val x: Int, val y: Int) : Event data class Key(val code: Int) : Event fun parse(raw: Map<String, Any?>): Event = when (raw["type"]) { "click" -> Click(raw.int("x"), raw.int("y")) "key" -> Key(raw.int("code")) else -> error("unknown event") } ``` After `parse`, downstream code dispatches on a `sealed` type with exhaustive `when` — **zero casts**, compiler-checked. Libraries like **kotlinx.serialization** do this conversion declaratively, eliminating hand-rolled casting entirely. ### Generics and erasure In hot paths watch for `as? List<Foo>` / `as? Map<K,V>`: erasure makes the argument unchecked, so a 'successful' cast can defer a `ClassCastException` to the element use site. Use `filterIsInstance` or `reified` inline helpers, or—better—let the typed parser produce the right element types. ### When to keep `as?` Keep it where ambiguity is real and local: a single heterogeneous external value, framework callbacks handing you `Any`, or interop with untyped Java APIs. The goal isn't zero `as?` — it's confining it to the boundary so the typed core stays cast-free. ### Checklist - Is a mismatch a *default* or an *error*? Make it explicit. - Could a typed model / sealed hierarchy remove the cast entirely? - Are generic casts erasure-unsafe? - Did I profile before treating the cast as a cost?
- Why can `as? Int ?: default` be dangerous in a config parser?A wrong-typed value (e.g. a numeric string) fails the cast and silently yields the default, masking malformed input instead of rejecting it. Decide explicitly whether mismatch means default or error.
- What does 'parse, don't validate' mean here?Convert untyped input into a precise typed model once at the boundary, so downstream code works with real types and needs no further casts or re-checks.
as?/Elvis everywhere is like patching leaks one drip at a time; redesigning to typed parsing is fixing the pipe at the inlet so the rooms downstream stay dry.
saying these in an interview costs you the question
- Treating `as?` as a meaningful performance cost without profiling
- Defending pervasive deep `as?` as good design
- Not noticing that `as? ?: default` can hide bad input
- Ignoring erasure for `as? List<T>` in hot loops
- Reaching for casts instead of sealed types / typed parsers