skip to content

Explain how the star projection maps to bounds: for `Foo<out T : TUpper>` and `Foo<in T>`, what does `Foo<*>` become for reading and writing?

level: middleimportance: must knowfreq 60%

answer

  1. out T -> out upper bound (Any? if none)
  2. in T -> in Nothing (can't write)
  3. invariant -> out Any? AND in Nothing
  4. Box<out T:Number> reads as Number
  5. Each type parameter projected independently

basics

~20 s

For an out parameter, Foo<*> reads as the parameter's upper bound (Any? if none). For an in parameter, Foo<*> only accepts Nothing for writes, so you effectively can't write. Star = read at upper bound, write nothing.

solid answer

~40 s

Star projection translates the unknown argument into safe bounds based on declaration-site variance: - For a covariant `out T` (where `T` appears only in out-positions), `Foo<*>` is equivalent to `Foo<out TUpper>`, so values come *out* typed as the declared upper bound `TUpper` (or `Any?` when unbounded). You can read but not write. - For a contravariant `in T`, `Foo<*>` is equivalent to `Foo<in Nothing>`, so the only acceptable input type is `Nothing` — you can't pass any real value in, but reads come back at `Any?`. - For an **invariant** `T` (e.g. `MutableList<T>`), `Foo<*>` is `Foo<out TUpper>` for reads and `Foo<in Nothing>` for writes simultaneously: read at the upper bound, can't write. This is the classic `out Any? / in Nothing` summary.

code

kotlin · 13 lines
kotlin
class Producer<out T : CharSequence>(private val items: List<T>) {
    fun get(i: Int): T = items[i]
}

fun useStar(p: Producer<*>) {
    val cs: CharSequence = p.get(0)  // projected to upper bound CharSequence
    println(cs.length)
}

fun main() {
    useStar(Producer(listOf("hello")))            // Producer<String>
    useStar(Producer(listOf(StringBuilder("x"))))  // Producer<StringBuilder>
}

go deeper

for a junior

Recalls the slogan 'out Any? / in Nothing' even if shaky on bounds.

for a middle

Correctly applies upper bounds to the read type and explains why writes are in Nothing.

for a senior

Handles invariant types, bounded parameters, and per-parameter projection precisely.

for a principal

Connects the bound rules to existential type elimination and recursive/F-bounded parameters like T : Comparable<T>.

## The core rule A star projection is shorthand for replacing the unknown type argument with the **safe extremes** of its position, per the declared variance of the parameter: | Declaration | `Foo<*>` for reading (out) | `Foo<*>` for writing (in) | |---|---|---| | `out T` (covariant) | `Foo<out TUpper>` | n/a (can't write anyway) | | `in T` (contravariant) | `Foo<out Any?>` | `Foo<in Nothing>` | | `T` (invariant) | `Foo<out TUpper>` | `Foo<in Nothing>` | Where **`TUpper`** is the declared upper bound of `T` (e.g. `T : Number` → `Number`), or **`Any?`** when there is no explicit bound. ## Why these two extremes - **Reading** pulls a value *out*. The widest type any unknown `T : TUpper` is guaranteed to be assignable to is `TUpper` (or `Any?`). So out-positions project to the **upper bound** — `out Any?` in the unbounded case. - **Writing** pushes a value *in*. The only value guaranteed to be a valid `T` for *every* possible unknown `T` is a value of type **`Nothing`** — which has no instances. So in-positions project to `in Nothing`: nothing can be written. That is the slogan: **star projection = `out Any?` / `in Nothing`.** ## Bounded example ```kotlin class Box<out T : Number>(val value: T) fun readNumber(b: Box<*>) { val n: Number = b.value // projected to Box<out Number>: reads as Number println(n.toDouble()) } readNumber(Box(42)) // Box<Int> readNumber(Box(3.14)) // Box<Double> ``` Because `T`'s upper bound is `Number`, `Box<*>.value` is typed `Number`, not `Any?`. ## Invariant example ```kotlin fun mutate(list: MutableList<*>) { val item: Any? = list[0] // read: out Any? // list.add(item) // ERROR: write is in Nothing -> nothing fits list.removeAt(0) // OK: doesn't take a T as input list.clear() // OK } ``` `MutableList<T>` is invariant, so `MutableList<*>` is simultaneously `out Any?` (reads) and `in Nothing` (writes). ## Multiple parameters Each type parameter is projected independently. `Map<String, *>` keeps `String` keys concrete and stars only the value type; `Map<*, *>` stars both.

  • If a class is declared `class C<T : Comparable<T>>`, what does `C<*>` read as?
    At the upper bound, but the bound references `T`, so it is recursively star-projected to `Comparable<*>` — not `Any?`.
  • Why is the write side `in Nothing` rather than `in Any?`?
    The actual argument could be any specific subtype; only a value assignable to *every* possible `T` is safe to write, and the only such type is `Nothing`, which has no instances — so effectively nothing can be written.

A one-way turnstile: outbound, anything may exit at the widest allowed type; inbound, the gate is set to Nothing, so no real value ever gets through.

saying these in an interview costs you the question

  • Saying reads are always `Any?` even when there's an explicit upper bound
  • Claiming you can write at `Any?` to a starred in/invariant position
  • Not knowing `Nothing` is the in-bound and why
  • Treating `out` and `in` parameters identically under star
  • Projecting all type parameters together rather than independently

context