skip to content

How do val and var differ for class properties (getters/setters), and why can a var property block a smart cast?

level: seniorimportance: should knowfreq 64%

answer

  1. val property = getter only; var = getter + setter
  2. Smart cast needs a STABLE value
  3. var, open val, custom-getter val, delegated = unstable
  4. Fix: capture into a local val before the check
  5. Custom-getter val can return different values each call

basics

~20 s

A val property only gets a getter; a var property gets a getter and a setter. A var (especially a public or open one) can change between a null check and its use, so the compiler refuses to smart-cast it because the value might no longer be non-null.

solid answer

~50 s

As properties, `val` generates a getter only, while `var` generates both getter and setter. A `val` property may still have a custom getter that computes a value each call (no backing field), but it cannot be reassigned externally. Smart-casting depends on the compiler proving the value can't change between the check and the use. A local `val` is provably stable, so `if (x != null) x.length` smart-casts. A mutable `var` — and especially a property — may be changed concurrently or via a custom getter that returns different values each call, so the compiler conservatively refuses the smart cast and you must use `?.`, `?: ` , `!!`, or capture it in a local `val`. Open `val` properties also block smart-casts because a subclass could override the getter. Stability categories the compiler tracks: local vals (stable), final non-custom-getter vals, vs. var/open/delegated/custom-getter (unstable).

code

kotlin · 6 lines
kotlin
interface Node { val next: Node? }
fun traverse(n: Node) {
    // n.next is an interface property -> not stable
    val nx = n.next            // capture into local val
    if (nx != null) traverse(nx)  // smart cast works on the local val
}

go deeper

for a junior

Knows val property has a getter and var adds a setter.

for a middle

Can explain a basic smart cast and that a var property can break it.

for a senior

Enumerates the stability rules (var, open, custom getter, delegated) and applies the local-val capture idiom.

for a principal

Reasons about thread-safety implications of mutable properties and designs APIs to keep values stable/smart-castable.

## Properties: what gets generated Kotlin properties auto-generate accessors: - **`val` property** → a `get()` only. - **`var` property** → `get()` **and** `set()`. ```kotlin class User(val id: Long, var name: String) // id: getter only; name: getter + setter ``` A `val` can still have a **custom getter** that returns a computed value with no backing field: ```kotlin class Circle(val r: Double) { val area: Double get() = Math.PI * r * r // recomputed each access } ``` Note: such a custom-getter `val` is **not stable** for smart-casting because each call could in principle return a different value. ## Smart casts, briefly A **smart cast** lets the compiler treat a value as a more specific (or non-null) type after a check, without an explicit cast: ```kotlin fun len(s: String?): Int { if (s != null) return s.length // s smart-cast to String return 0 } ``` This is only sound if the value **cannot change** between the check and the use. ## Why var (and some vals) block smart casts The compiler tracks **stability**. A smart cast is allowed only for *stable* values: - **Stable:** local `val`, `private`/`internal` `val` with a default backing field (no custom getter), function parameters. - **Unstable (no smart cast):** - **`var`** — could be reassigned, including by another thread, between check and use. - **`open val`** — a subclass might override the getter to return varying values. - **`val` with a custom getter** — could return a different value each call. - **delegated properties** (`by lazy`, `by Delegates.observable`). - **mutable properties of another module/package** the compiler can't prove are stable. ```kotlin class Box { var value: String? = null } fun f(b: Box) { if (b.value != null) { // b.value.length // ERROR: smart cast impossible, b.value is a mutable property b.value?.length // fix 1: safe call val v = b.value; if (v != null) v.length // fix 2: capture in local val } } ``` ## Practical fixes - Capture the property into a **local `val`** before the check (most idiomatic). - Use the safe-call `?.` operator. - Use `?:` Elvis for a default. - `!!` asserts non-null but throws if wrong — last resort. ## Takeaway `val` vs `var` on properties isn't just about reassignment ergonomics — it directly affects what the compiler can prove, and therefore the cleanliness of your null-handling and casting code.

  • Why does an open val block a smart cast even though it can't be reassigned?
    A subclass can override its getter to return different values on successive calls, so the compiler can't prove stability between the check and the use.
  • What's the most idiomatic fix when a property won't smart-cast?
    Assign it to a local val first; the local is stable and smart-casts cleanly.

saying these in an interview costs you the question

  • Claiming all vals always smart-cast
  • Not knowing var properties block smart casts
  • Reaching for !! as the default fix instead of a local val or ?.
  • Thinking a val property never has a getter (it always does)
  • Believing custom-getter vals are stable for smart-casting

context