skip to content

Given `listOf("a", null, "b")` and `listOf(1, "x")`, what element types does Kotlin infer, and how does nullable unification combine with generic variance to produce them?

level: middleimportance: should knowfreq 45%

answer

  1. listOf infers T = LUB of all elements
  2. null among strings -> List<String?>
  3. Int + String -> List<Any> (intersection)
  4. List<out E> is covariant: List<String> <: List<String?>
  5. List<String?> vs List<String>? are different

basics

~20 s

listOf("a", null, "b") is List<String?> because the elements unify to String?. listOf(1, "x") is List<Any> because Int and String share only the supertype Any (or its intersection). The common type of all elements becomes the element type.

solid answer

~30 s

`listOf(vararg elements: T)` infers `T` as the **least upper bound** of all argument types. For `listOf("a", null, "b")`: arguments are `String`, `Nothing?`, `String`; `LUB(String, Nothing?) = String?`, so the result is `List<String?>`. For `listOf(1, "x")`: `Int` and `String` are unrelated, so the LUB climbs to their common supertype `Any` (in practice the intersection `Comparable<*> & Serializable`, but conceptually `Any`), giving `List<Any>`. Because `List` is **covariant** (`interface List<out E>`), a `List<String>` is also a `List<String?>` and a `List<Nothing>` is a `List<Any>` — variance lets the inferred element type widen naturally. The same LUB machinery that types `if/else` branches types vararg/collection literals.

code

kotlin · 5 lines
kotlin
val a = listOf("a", null, "b")   // List<String?>
val b = listOf(1, "x")           // List<Any>
val c = listOf(1, 2, 3)          // List<Int>
val e: List<String?> = listOf("x")  // List<String> widens via out E
val f: List<Any> = emptyList<Nothing>() // List<Nothing> <: List<Any>

go deeper

for a junior

Recognizes that adding null gives a list of nullable elements (List<String?>).

for a middle

Explains element type = LUB of elements and that unrelated types climb to Any, plus the List<String?> vs List<String>? distinction.

for a senior

Ties LUB inference to covariance (out E) enabling sound widening like List<String> <: List<String?>.

for a principal

Discusses intersection-type computation for unrelated elements and design tradeoffs of declaration-site variance in the standard library collections.

## Vararg inference uses LUB on elements `listOf` is `fun <T> listOf(vararg elements: T): List<T>`. The compiler infers the single type parameter `T` as the **least upper bound (LUB)** — the most specific common supertype — of every argument's type. ## Case 1: a null among strings ```kotlin val xs = listOf("a", null, "b") ``` - `"a"`, `"b"`: type `String`. - `null`: type `Nothing?`. - `LUB(String, Nothing?, String) = String?`. Result: `List<String?>`. The single `null` lifts the element type into the nullable half of the lattice — exactly the same upward unification as in `if/else`. ## Case 2: unrelated element types ```kotlin val ys = listOf(1, "x") ``` - `1`: `Int`; `"x"`: `String`. - These are unrelated, so the LUB climbs to their nearest common supertype. Conceptually that is `Any`; the compiler may compute the **intersection of shared supertypes** (`Comparable<*> & java.io.Serializable`), but for an explicit annotation you'd write `List<Any>`. Result: `List<Any>` (no nullability, since neither element is null). ## How variance participates `List` is declared **covariant**: `public interface List<out E>`. Covariance (`out`) means: if `A <: B` then `List<A> <: List<B>`. This is what lets the inferred element type *widen*: - `List<String> <: List<String?>` (since `String <: String?`), so a list of strings is usable as a list of nullable strings. - `List<Nothing> <: List<Any>`, so `emptyList<Nothing>()` works anywhere a `List<Any>` is needed. Without covariance, widening the element type would be unsound; with it, the LUB-inferred type integrates cleanly. ## A nullable-collection vs collection-of-nullable distinction Be careful: - `List<String?>` — a non-null list whose **elements** may be null. - `List<String>?` — a **list reference** that may be null, elements non-null. - `List<String?>?` — both may be null. Unification of elements only affects the *element* type (`String?`); the outer list reference here is non-null. ## Summary Element type = LUB of all elements; a single `null` (type `Nothing?`) makes it nullable; unrelated types climb toward `Any`; covariance (`out E`) makes the resulting widening type-safe.

  • What is the difference between List<String?> and List<String>?
    List<String?> is a non-null list whose elements may each be null. List<String>? is a possibly-null reference to a list of non-null strings. The ? attaches to different positions.
  • Why can a List<String> be passed where a List<String?> is expected?
    List is covariant (out E), and String <: String?, so List<String> <: List<String?>. Covariance propagates the element subtype relation to the container.

saying these in an interview costs you the question

  • Saying listOf("a", null) is a compile error
  • Inferring List<String> (dropping nullability) for listOf("a", null)
  • Confusing List<String?> with List<String>?
  • Claiming List is invariant and so cannot widen the element type

context