skip to content

How does the nullable lattice handle generic type parameters whose bound is non-null (e.g. `<T : Any>` vs `<T>`), and what happens to nullability when a type parameter T is itself nullable and you write T? — is there a 'double nullable' T??

level: seniorimportance: nice to knowfreq 30%

answer

  1. Unbounded <T> has bound Any? (can be null)
  2. <T : Any> forces non-null type argument
  3. T? = nullable form of T
  4. Nullability is idempotent: no String?? / no T??
  5. Platform type T! at the Java boundary

basics

~10 s

An unbounded T defaults to bound Any?, so T can already be nullable. Writing T? just guarantees nullability. There is no T?? — nullable is idempotent: making an already-nullable type nullable again changes nothing.

solid answer

~40 s

An unbounded type parameter `<T>` has an implicit upper bound of `Any?`, so `T` may be substituted with a nullable type — meaning a value of type `T` might already be null. To forbid that, declare `<T : Any>`, giving a non-null bound. Within generics, `T?` is the *nullable* form of `T`: if `T = String`, `T? = String?`; if `T = String?` (already nullable), then `T?` is still just `String?` — **nullability is idempotent**, so there is no distinct `T??`/`String??`. The lattice flattens repeated `?`. This is why `fun <T> firstOrNull(): T?` returns the same type whether `T` is nullable or not, and why a bound `<T : Any>` is the idiomatic way to force callers to supply non-null type arguments (e.g. `requireNotNull` returns `T` from a `T?` input bounded by `Any`).

code

kotlin · 6 lines
kotlin
fun <T> idNullable(x: T): T = x        // T bound is Any?, x may be null
fun <T : Any> idNonNull(x: T): T = x   // T bound is Any, x is non-null

fun <T> first(list: List<T>): T? = list.firstOrNull()
val a = first(listOf("x"))             // String?
val b = first(listOf<String?>("x"))    // still String?, not String??

go deeper

for a junior

Knows you can add ? to make a type nullable and that you cannot stack ? twice.

for a middle

States that unbounded <T> defaults to Any? and <T : Any> forces non-null, and that T? is T's nullable form.

for a senior

Explains nullability idempotency (no T??), the two-layer lattice, and how the default Any? bound affects generic null-safety.

for a principal

Discusses platform types at the Java boundary, how idempotency keeps LUB/smart-cast reasoning tractable, and API design with <T : Any> bounds.

## Default bound of a type parameter When you write a generic without an explicit bound: ```kotlin fun <T> identity(x: T): T = x ``` the parameter `T` has an **implicit upper bound of `Any?`**, not `Any`. That means `T` can be instantiated with a *nullable* type, so a value of declared type `T` **might be null**. You cannot call `.length` etc. on it without a null check. ## Forcing non-null with `<T : Any>` To require that the type argument be non-null, give an explicit non-null bound: ```kotlin fun <T : Any> requireNonNull(x: T): T = x ``` Now `T` ranges only over non-null types, and a `T` value is guaranteed non-null. The standard library uses this, e.g. `requireNotNull(value: T?): T` narrows a nullable input to a non-null `T` bounded by `Any`. ## `T?` inside generics `T?` means 'the nullable form of whatever `T` is': - If `T = String`, then `T? = String?`. - If `T = Int`, then `T? = Int?`. ## Idempotent nullability — no `T??` Key lattice property: **nullability is idempotent**. Applying `?` to an already-nullable type does nothing: ```kotlin // conceptually String?? == String? ``` So if `T` is substituted with `String?`, then `T?` is **still** `String?`, not some doubly-nullable `String??`. There is exactly one `null` and one nullable layer. You literally cannot write `String??` in source — the lattice collapses repeated `?`. ```kotlin fun <T> List<T>.firstOrNull(): T? = if (isEmpty()) null else this[0] val a = listOf("x").firstOrNull() // String? val b = listOf<String?>("x").firstOrNull() // T = String?, T? = String? (still single ?) ``` Both return `String?`. ## Why this matters for the lattice The nullable lattice has at most **one** nullable level per underlying type: `T` and `T?`, with `T <: T?`. There is no third tier. When generics could otherwise stack `?`, idempotency keeps the lattice two-layered. This guarantees that `null` checks, smart casts, and LUB computations don't have to reason about arbitrarily-deep nullability. ## Platform types (interop edge) From Java, a type with unknown nullability arrives as a **platform type** `T!`, which is flexibly *either* `T` or `T?`. Platform types are not something you write; they relax the lattice at the Java boundary and let you choose the nullability when you assign. (Mentioned for completeness — the pure-Kotlin lattice itself only has `T` and `T?`.) ## Summary - Unbounded `<T>` => bound `Any?` => `T` may be nullable. - `<T : Any>` => non-null bound. - `T?` = nullable form of `T`; idempotent, so no `T??`. - The lattice is strictly two-layered per type.

  • If T is already nullable (T = String?), what is T??
    Still String?. Nullability is idempotent, so applying ? to an already-nullable type adds nothing — there is no String??.
  • What is the implicit upper bound of an unbounded type parameter, and why does it matter?
    Any?. It means an unbounded T may be substituted with a nullable type, so a T value can be null; you need <T : Any> to forbid that.

Marking something nullable is like flipping a single switch — flipping it again when it's already on does nothing.

saying these in an interview costs you the question

  • Believing T?? or String?? is a distinct doubly-nullable type
  • Saying an unbounded <T> defaults to Any (non-null) instead of Any?
  • Thinking <T : Any> is the same as unbounded <T>
  • Claiming you can stack multiple ? to get deeper nullability
  • Confusing platform type T! with the regular nullable T?

context