skip to content

Explain the subtype relationship between T and T? in Kotlin. Which direction of assignment is allowed and why?

level: middleimportance: should knowfreq 64%

answer

  1. T <: T? (subtype, not supertype)
  2. Widening non-null -> nullable is free
  3. Narrowing nullable -> non-null needs proof
  4. Liskov: subtype usable where supertype expected
  5. Smart cast / ?: / !! to go down

basics

~10 s

T is a subtype of T?, so a non-null value fits anywhere a nullable is expected. The other way doesn't work without a check, because a nullable might actually be null.

solid answer

~50 s

For any type T, the relation T <: T? holds — T is a subtype of T?. Conceptually T's value set is a subset of T?'s value set (the same values, minus null). By the Liskov substitution principle, a value of a subtype can be used wherever the supertype is expected, so you can always pass a String where String? is wanted: the assignment widens the type and is implicit and safe. The reverse direction, String? to String, is a narrowing and is rejected by the compiler because the nullable value might be null — you must prove non-nullness first via a smart-cast (after an if (x != null) check), the Elvis operator ?:, or the !! assertion. This asymmetry is what makes nullability composable: non-null values flow freely 'up', and the compiler gates every move 'down'.

code

kotlin · 8 lines
kotlin
fun describe(x: String?): String = x ?: "<none>"

val name: String = "Ada"
describe(name)          // String -> String? widening, ok

val maybe: String? = null
// val sure: String = maybe   // error
val sure: String = maybe ?: "fallback"

go deeper

for a junior

Recognizes you can pass a non-null where nullable is wanted but not the reverse.

for a middle

States T <: T? precisely and explains widening vs narrowing via value sets and Liskov.

for a senior

Connects it to smart casts, Any? as top type, and how variance propagates the relation through generics.

for a principal

Reasons about how the subtype rule keeps nullability composable and the soundness guarantees it provides across the type lattice.

## The relation: T <: T? Kotlin defines, for every type `T`, that **`T` is a subtype of `T?`** (written `T <: T?`). Intuitively: - The value set of `T` = all real instances. - The value set of `T?` = all real instances **+ `null`**. Since `T`'s values are a strict subset of `T?`'s values, every `T` is also a valid `T?`. Hence `T <: T?`. ## What the subtype relation lets you do By the **Liskov substitution principle**, a subtype value is usable wherever the supertype is expected. So **widening** (non-null → nullable) is implicit and always safe: ```kotlin val nonNull: String = "hi" val nullable: String? = nonNull // ok: String <: String? fun accept(x: String?) {} accept("hi") // ok: String passed where String? expected ``` ## What it forbids The reverse, **narrowing** (nullable → non-null), is **not** automatic, because a `T?` could be `null` and `null` is not a member of `T`: ```kotlin val nullable: String? = maybeGet() // val nonNull: String = nullable // COMPILE ERROR val nonNull: String = nullable ?: "" // elvis fallback val also: String = nullable!! // assertion (throws if null) if (nullable != null) { val s: String = nullable // smart cast: String? -> String } ``` ## How narrowing is achieved - **Smart cast**: after `if (x != null)`, the compiler narrows `x` from `T?` to `T` inside that scope (works for `val` and stable local `var`). - **Elvis `?:`**: `x ?: default` has type `T` when `default` is `T`. - **`!!`**: asserts non-null, narrowing the type but risking `NullPointerException`. ## Generic / variance note The relation composes with variance: e.g. `List<String>` is a subtype of `List<String?>` because `List` is covariant in its element type and `String <: String?`. The same one-directional rule applies — covered more in the 'Nullability in Generics' sibling topic. ## Why the asymmetry is the point Non-null values can always be 'promoted' to nullable for free, but every step from nullable down to non-null is gated by an explicit, compiler-checked proof of non-nullness. This is exactly what eliminates accidental NPEs.

  • Is Any a supertype of Any?, or the other way around?
    Any? is the supertype: Any <: Any?. Any? is the top of the entire Kotlin type hierarchy because it includes null and every other value.
  • Does the relation T <: T? mean String? and String share the same methods?
    No. You can call String's members directly only on String. On String? you must use ?. or narrow first, because the receiver might be null.

Going from non-null to nullable is downhill (automatic); going back up requires you to show a ticket proving it isn't null.

saying these in an interview costs you the question

  • Claiming T? is a subtype of T (it's reversed)
  • Saying you can freely assign nullable to non-null
  • Not knowing widening is implicit while narrowing needs proof
  • Confusing the relation with variance rules but unable to state the base case

context