skip to content

How do `&&` and `||` enable smart casting within a single boolean expression?

level: middleimportance: should knowfreq 60%

answer

  1. `&&`: left guard -> right narrowed
  2. `||`: left negative -> right narrowed
  3. `x != null && x.foo()` compiles
  4. `x == null || x.foo()` compiles
  5. Narrowing flows into the matching branch

basics

~20 s

With &&, a check on the left makes the variable narrowed on the right, because the right side only runs if the left was true. With ||, the right side runs only when the left was false, so the opposite narrowing applies.

solid answer

~50 s

Kotlin smart-casts within short-circuiting boolean expressions. For `a && b`, `b` is only evaluated when `a` is true, so any narrowing established by `a` (like `x != null` or `x is String`) is in effect while evaluating `b`. Example: `if (x != null && x.length > 0)` — the `x.length` access compiles because the right operand sees `x` as non-null. For `a || b`, `b` is only evaluated when `a` is false, so a *negated* narrowing flows in: `if (x == null || x.length == 0)` smart-casts `x` to non-null in the right operand. The same applies to the branches taken after the whole condition: a true `&&` chain carries all its narrowings into the `then` block; a `||` of negatives carries them into the `else` / early-return path. This is the standard idiom for combining a null/type guard with a follow-up condition on the same line.

code

kotlin · 7 lines
kotlin
fun classify(x: Any?): String {
    // && : right operand sees x as String
    if (x is String && x.startsWith("k")) return "k-string"
    // || : right operand sees x as non-null
    if (x == null || x.hashCode() == 0) return "nullish"
    return "other"
}

go deeper

for a junior

Knows x != null && x.length > 0 compiles and can write such a guard.

for a middle

Explains short-circuiting drives it, and that || narrows the right operand via the negated case.

for a senior

Reasons about how narrowings flow into then/else branches and combines multiple operands, and notes & vs && matters.

for a principal

Generalizes to boolean flow analysis (each operand contributes to a path-condition) and ties stability to soundness when operands have side effects.

## Short-circuit evaluation is the mechanism Kotlin's `&&` and `||` are **short-circuiting**: the right operand is evaluated only when needed. The smart-cast machinery piggybacks on that fact. ## `&&` — left guard narrows the right ```kotlin fun nonEmpty(x: String?): Boolean { return x != null && x.length > 0 // x is String in the right operand } ``` `x.length` is reachable only when `x != null` already evaluated to true, so `x` is smart-cast to `String` while evaluating the right side. If the whole `&&` is used as an `if` condition, the narrowing also flows into the `then` branch: ```kotlin if (x is String && x.length > 3) { println(x.uppercase()) // x: String here too } ``` ## `||` — left negative narrows the right ```kotlin fun isEmptyOrBlank(x: String?): Boolean { return x == null || x.isBlank() // x is String in the right operand } ``` The right operand of `||` runs only when the left was **false**. So after `x == null` is false, `x` is non-null while evaluating `x.isBlank()`. Equivalently, `x !is String || x.length == 0` narrows `x` to `String` on the right. ## Carrying into branches / returns Because of De Morgan-style flow reasoning: - A `&&` chain of positive checks narrows the **then** branch. - A `||` chain of negative checks narrows the **else** branch and any early-exit after it: ```kotlin fun handle(x: Any?) { if (x == null || x !is String) return println(x.length) // x: String after the guard } ``` ## Nesting and combining ```kotlin fun pick(a: Any?, b: Any?): Int { if (a is String && b is String) { return a.length + b.length // both narrowed } return 0 } ``` Each operand contributes an independent narrowing visible to later operands and the chosen branch. ## Caveat The variables must be **stable** (typically `val`); a custom getter or a `var` mutated between operands can defeat it (the *limitations* sibling). Keywords/operators: `&&`, `||`, `is`, `!is`, `== null`, short-circuit.

  • Why doesn't `x != null & x.length > 0` (single `&`) smart-cast the right side?
    Single `&` is the non-short-circuiting bitwise/boolean `and` that always evaluates both sides, so the compiler can't assume the left was true when the right runs; use `&&`.
  • In `if (x == null || x !is String) return`, what is x afterward?
    `String` (non-null) — the guard exits whenever x is null or not a String, so the surviving path has x as a non-null String.

Relay handoff: && passes the 'it's valid' baton rightward; || passes the 'it's invalid, so check the next' baton rightward.

saying these in an interview costs you the question

  • Thinks `||` narrows the right operand the same way `&&` does (forgets it's the negated branch)
  • Uses single `&`/`|` and expects smart casting
  • Cannot explain that short-circuit evaluation order is what makes it work
  • Believes the narrowing doesn't reach the then/else branch

context