skip to content

How Kotlin Models Absence

Kotlin encodes absence in the type itself — String versus String? — instead of wrapping values in an Option or Maybe. The comparison interviewers want is why a compile-time type split is cheaper and more ergonomic than a runtime wrapper, and where it still leaks.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

In Kotlin, what is the difference between the types `String` and `String?`, and what does the compiler do differently for each?

level: juniorimportance: must knowfreq 90%

answer

  1. `?` appended = the nullable type
  2. `String` <: `String?` (one-way subtype)
  3. null in non-null type = compile error
  4. can't dereference `T?` directly
  5. not a wrapper — same JVM type, zero cost

basics

~10 s

String can never hold null; String? can. The compiler rejects assigning null to String, and forces you to handle the null case before using a String? directly.

solid answer

~40 s

Kotlin splits every type into a non-nullable form (`String`) and a nullable form (`String?`, the type with `?` appended). The two are distinct compile-time types: `String` is a subtype of `String?`, so a non-null value flows into a nullable slot freely, but not the reverse. Assigning `null` to a `String` is a compile error. A `String?` cannot be dereferenced directly — the compiler forbids `s.length` until you prove non-nullness (via `?.`, a null check, `!!`, etc.). This makes nullability part of the static type, so most NullPointerExceptions become compile errors instead of runtime crashes. Under the hood both compile to the same JVM reference type (`java.lang.String`); the `?` is compile-time information, not a wrapper object.

code

kotlin · 7 lines
kotlin
val a: String = "hi"      // never null
val b: String? = null      // may be null

// val c: String = null    // compile error
// val len = b.length      // compile error: b is String?
val len: Int? = b?.length  // OK: produces Int?
val up: String? = a        // OK: String is a subtype of String?

go deeper

for a junior

Knows String? allows null and String does not, and that the compiler stops you assigning null to non-null.

for a middle

Explains the subtype relationship (String <: String?) and that you can't dereference a nullable directly.

for a senior

Frames it as moving NPEs from runtime to compile time, and knows it's compile-time metadata, not a runtime wrapper.

for a principal

Connects it to API design intent (signatures as contracts) and contrasts the zero-cost type-system approach with wrapper-based Option types.

## The core idea Kotlin encodes *whether a value can be absent* directly in its **static type**. For every type `T` there is a **non-nullable** type `T` (e.g. `String`) and a **nullable** type `T?` (e.g. `String?`). The trailing `?` is a type constructor: it produces a new type that includes `null` as a legal value. ## What the compiler enforces - A `String` variable can **never** be `null`. `val s: String = null` is a **compile error**, not a runtime failure. - A `String?` variable **may** be `null`. The compiler therefore **refuses to let you dereference it directly**: `val n: Int = s?.length` is fine, but `s.length` on a `String?` is a compile error ("only safe (`?.`) or non-null asserted (`!!`) calls are allowed"). - This converts a huge class of `NullPointerException`s from *runtime* crashes into *compile-time* errors. ## Subtype relationship `String` is a **subtype** of `String?`. So this is one-directional: ```kotlin val nonNull: String = "hi" val nullable: String? = nonNull // OK: String <: String? // val back: String = nullable // ERROR: String? is not a subtype of String ``` The same applies to every type, including generic ones and `Any`/`Any?`. `Any?` is the top of the whole type hierarchy (everything, including null); `Any` is everything *except* null. ## Not a wrapper Unlike Java's `Optional<String>`, Scala's `Option[String]`, or Rust's `Option<String>`, `String?` is **not a wrapper object**. There is no boxing, no `.get()`, no extra allocation. On the JVM, both `String` and `String?` erase to the same `java.lang.String` reference; the distinction lives only in the compiler (and in `@Nullable`/`@NotNull` metadata it emits). So you pay zero runtime cost for nullability tracking. ## Why this matters - The type signature documents intent: a function returning `User?` *tells* callers it may not find a user. - The compiler mechanically forces callers to deal with that case. - Null becomes a **first-class, tracked value** in the type system rather than a hidden landmine.

  • Is `String?` a different class at runtime than `String`?
    No. On the JVM both erase to `java.lang.String`. The nullability is compile-time only, exposed via `@Nullable`/`@NotNull` metadata; there is no wrapper object.
  • Can you pass a `String` where a `String?` is expected?
    Yes. `String` is a subtype of `String?`, so non-null values flow into nullable slots. The reverse needs an explicit null check or assertion.

String? is like a labelled box that might be empty; String is a box guaranteed full — the label is checked at the factory (compile time), not when you open it.

saying these in an interview costs you the question

  • Claiming `String?` is a wrapper like `Optional<String>`
  • Saying assigning null to `String` fails at runtime (it's compile-time)
  • Thinking `String` and `String?` are unrelated types with no subtype link
  • Believing the `?` adds boxing or allocation overhead

context

open as a page

How does Kotlin's approach to absence differ from wrapper-based Option/Maybe types (e.g. Java `Optional`, Scala `Option`, Rust `Option`)?

level: middleimportance: should knowfreq 60%

basics

~10 s

Option/Maybe wrap a value in an extra object you must unwrap. Kotlin instead marks the type itself as nullable, so null stays the plain value — no wrapper, no unwrapping, checked by the compiler.

open as a page

How does nullability interact with generics in Kotlin? Explain what `T`, `T?`, and `T & Any` mean for a type parameter, and how to express a non-null bound.

level: seniorimportance: should knowfreq 30%

basics

~20 s

An unbounded type parameter T can itself be a nullable type, so values of T might be null. To force non-null, bound it with T : Any. Writing T? always allows null; T & Any strips nullability from a T.

open as a page

What is the type of the literal `null` in Kotlin, and how do `Nothing`, `Nothing?`, `Any`, and `Any?` fit into the type hierarchy with respect to nullability?

level: seniorimportance: should knowfreq 35%

basics

~20 s

null's type is Nothing?. Nothing is the empty type at the bottom of every type; Nothing? holds only null. Any? sits at the very top (all values including null); Any is the top of all non-null values.

open as a page

What are the architectural benefits and the limitations of modeling absence in the type system (compile-time) rather than at runtime, especially regarding Java interop and reflection/generics?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Putting null in the type system catches missing-value bugs at compile time and documents intent for free. The catch: it's compiler-only, so Java code, reflection, and erased generics can slip null past the guarantees at runtime.

open as a page