skip to content

When should you use a star projection `List<*>` versus an explicit use-site projection like `List<out Number>` or a generic type parameter `<T>`?

level: seniorimportance: should knowfreq 45%

answer

  1. Relate in/out positions -> generic <T>
  2. Need a useful bound -> out Bound / in Bound
  3. Type truly irrelevant -> star *
  4. is concise but lossy; casting after * = wrong tool
  5. Star only some args: Map<String, *>

basics

~20 s

Use * only when you truly don't know or need the type — pure read/passthrough/counting. Use an explicit projection (out Number) when you need a useful upper bound. Use a generic <T> when callers need to relate input and output types.

solid answer

~50 s

Choose by how much you need to know about the type: - **Star `List<*>`**: you don't care about the element type at all. Best for utility/diagnostic code (`size`, logging, equality). It throws away type information, so anything you read is the (possibly wide) bound. - **Explicit use-site projection `List<out Number>`**: you don't need the exact type but you *do* need a meaningful bound — reads should be `Number`, and you want to accept `List<Int>`, `List<Double>`, etc. Strictly more informative than `*` when a bound exists. - **Generic parameter `fun <T> firstAndAdd(list: MutableList<T>, e: T)`**: you must *relate* multiple positions — read a `T` and write a `T`, or return the same `T` you accepted. `*` can't do this because it forgets the type; a captured type variable can. Rule of thumb: reach for `*` last — only when there is genuinely nothing useful to say about the type.

code

kotlin · 12 lines
kotlin
// * — type irrelevant
fun describe(c: Collection<*>) = "size=${c.size}"

// explicit projection — bound is useful
fun total(xs: List<out Number>) = xs.sumOf { it.toDouble() }

// generic — must relate read and write
fun <T> rotate(list: MutableList<T>) {
    if (list.isEmpty()) return
    val head = list.removeAt(0)
    list.add(head)
}

go deeper

for a junior

Knows * is for 'don't care about the type' cases like counting.

for a middle

Picks explicit out Bound over * when a bound helps and recognizes the equivalence to out Any?.

for a senior

Correctly chooses generic <T> to relate positions and refactors a stuck * API into a parameterized one.

for a principal

Reasons about API ergonomics, information loss, and when to expose <T> vs projections in a public surface.

## The decision ladder Pick the *most* informative option your code can justify, then fall back: 1. **Generic type parameter `<T>`** — when you need to *connect* type positions. If a value flows in and the same type flows out, or two arguments must share a type, you need a named `T`: ```kotlin fun <T> swapFirst(list: MutableList<T>, e: T): T { val old = list[0] list[0] = e // works: list and e share T return old } ``` Star projection cannot express this — `MutableList<*>` forbids the write and forgets that the read and the argument share a type. 2. **Explicit use-site projection `out`/`in`** — when you don't need the exact type but a *bound* is useful: ```kotlin fun sumAll(nums: List<out Number>): Double = nums.sumOf { it.toDouble() } // reads as Number ``` `List<out Number>` accepts `List<Int>`, `List<Long>`, etc., and reads come back as `Number` — richer than `Any?`. Use `in` for the symmetric consumer case (`MutableList<in String>` to add strings into a wider list). 3. **Star projection `<*>`** — when the type is genuinely irrelevant: ```kotlin fun logSize(c: Collection<*>) = println("items: ${c.size}") fun isEmpty2(m: Map<*, *>) = m.isEmpty() ``` Use `*` for counting, emptiness, identity/equality, or pure passthrough where you never touch a `T`-typed value meaningfully. ## Equivalences and subtleties - For an unbounded `out` parameter, `Foo<*>` is **equivalent** to `Foo<out Any?>`. Prefer the bare `*` for readability; prefer `out Bound` whenever a tighter bound exists. - `*` is concise but **lossy**. If you find yourself casting the result of a starred read, you probably wanted `out Bound` or `<T>`. - You can mix: `Map<String, *>` keeps the key type and stars only the value — better than `Map<*, *>` when keys matter. - For an `in`-projected consumer you usually want explicit `in T` (so you can write `T`), not `*` (which projects writes to `Nothing`). ## Anti-patterns - Using `*` then immediately `as`-casting back to a concrete type defeats the safety and signals you wanted a real generic parameter. - Declaring `<T>` with `T` used in exactly one position and never related elsewhere — that's an unnecessary type parameter; a projection (or `*`) is cleaner.

  • Is `List<*>` ever preferable to `List<out Any?>`?
    They're equivalent for an unbounded `out` parameter; `*` is just more concise and idiomatic. Prefer `out Bound` only when a tighter bound than `Any?` exists.
  • You wrote `fun copy(src: List<*>, dst: MutableList<*>)` and can't add elements. How do you fix it?
    Introduce a shared type parameter: `fun <T> copy(src: List<T>, dst: MutableList<in T>)`. Star forgets that source and destination share a type; a named `T` (with `in T` on the destination) restores the safe write.

Labeling a parcel: <T> writes the exact contents so you can re-pack the same thing; out Number writes a category; * writes only 'parcel' — fine if you never open it.

saying these in an interview costs you the question

  • Defaulting to `*` everywhere and casting reads back
  • Using `*` when input and output must share a type
  • Claiming `out Number` and `*` are equally informative when a bound exists
  • Adding an unused generic parameter where `*` would suffice
  • Not knowing `*` equals `out Any?` for unbounded covariant params

context