In Kotlin's type system, what is the relationship between a type T and its nullable counterpart T?, and why does assigning a String to a String? compile without any conversion?
answer
- T is a subtype of T?
- Values of T? = values of T plus null
- Widening to nullable is implicit, no conversion
- Narrowing T? -> T is rejected (may be null)
- Same JVM reference, nullability is compile-time only
basics
~20 sT is a subtype of T?. Every non-null String is also a valid String?, so a String fits anywhere a String? is expected, just like a child fits where a parent is asked for. No conversion happens.
solid answer
~40 sEach non-nullable type T is a strict subtype of its nullable form T?. The set of values of T? is exactly the values of T plus the single value null, so T's values are a subset of T?'s values. Because subtype-to-supertype assignment is always allowed (Liskov substitution), `val a: String? = "hi"` compiles with no boxing, wrapping, or conversion — the runtime representation is identical; nullability is a compile-time-only distinction the compiler tracks. The reverse (`val s: String = a`) is rejected because a String? may hold null, which String forbids. This T <: T? edge is the foundational link of the whole nullable lattice and is why widening to nullable is implicit while narrowing requires a smart cast or operator.
code
kotlin · 5 linesval s: String = "hi"
val n: String? = s // OK, T <: T? widening, no conversion
// val back: String = n // compile error: String? not a subtype of String
fun len(x: String?) = x?.length
len(s) // passing String where String? is expected: finego deeper
Knows T is a subtype of T? and that a non-null value can be used where nullable is expected without conversion.
Explains it via value sets (T's values are a subset of T?'s) and notes the runtime representation is identical for reference types.
Frames it as the foundational lattice edge enabling implicit widening and explains why narrowing is rejected via Liskov substitution.
Connects the edge to overall lattice design, primitive boxing nuance for Int?, and how the compiler models nullability as a compile-time-only distinction.
## The core relationship: T <: T? For any type `T`, Kotlin defines a **nullable** type `T?`. The key fact is a **subtype relationship**: `T` is a subtype of `T?` (written `T <: T?`). 'Subtype' means: anywhere a value of the supertype is expected, a value of the subtype is accepted. Why is `T <: T?`? Think in terms of the **set of values** each type can hold: - `String` can hold any string: `""`, `"hi"`, etc. - `String?` can hold all of those **plus** the special value `null`. So the values of `String` are a *subset* of the values of `String?`. A type whose values are a subset is a subtype. Hence every non-null `String` already *is* a legal `String?`. ## Why assignment needs no conversion ```kotlin val s: String = "hi" val n: String? = s // OK: widening T -> T?, implicit ``` There is **no boxing, no wrapper object, no runtime conversion**. Nullability is tracked by the compiler at compile time; at the JVM level both `s` and `n` are the same reference. The compiler simply *permits* `n` to also be `null` later. The reverse is rejected: ```kotlin val back: String = n // ERROR: String? is not a subtype of String ``` Because `n` could be `null`, and `String` excludes `null`, you cannot widen-then-narrow implicitly. ## How this scales the lattice This single edge `T <: T?` is repeated for every type, producing pairs like `Int <: Int?`, `List<T> <: List<T>?`, etc. Combined with normal subtyping it forms the **nullable lattice**: e.g. `String <: String?` and `String <: Any` both hold, and `String? <: Any?`. ## Related keywords - The `?` suffix marks a type as nullable. - A *non-null assertion* or smart cast is needed to go the other way (those operators belong to null-safety, not covered here). - `Nothing` sits below everything as the bottom type, including below every `T`. In short: `T? ` is `T` *widened* to also admit `null`, and widening (subtype to supertype) is always implicit.
- Is there any runtime representation difference between String and String? on the JVM?No. For reference types the bytecode is identical; nullability is compile-time metadata. (Primitives like Int may differ because Int? must box to java.lang.Integer, but the subtype relation still holds.)
- If T <: T?, why can't I assign a String? to a String directly?Subtyping only permits widening (subtype -> supertype). String? is the supertype here, so going String? -> String is narrowing and unsafe, since the value might be null.
T? is a box that holds everything T holds plus an 'empty' marker — every full box already qualifies as 'box-or-empty'.
saying these in an interview costs you the question
- Claiming T? is a subtype of T (the direction is reversed)
- Saying assigning T to T? performs a wrapping/boxing conversion for reference types
- Believing String and String? are unrelated, separate types
- Thinking the reverse assignment T? -> T is allowed without a check