skip to content

Contrast `List<*>`, `List<Any?>`, and a raw `List` (as seen from Java). What does each mean and how do they relate to erasure?

level: seniorimportance: nice to knowfreq 28%

answer

  1. List<*> = unknown fixed type (~List<out Any?>)
  2. List<Any?> = element type literally is Any?
  3. Raw List = Java untyped, Kotlin won't let you declare it
  4. Can read List<*> as Any?, can't add concrete to MutableList<*>
  5. List covariant: List<String> -> List<Any?> ok

basics

~20 s

List<*> is a list of some unknown type you can read from but not safely add to. List<Any?> is a list explicitly of anything. A raw List is the untyped Java view that erasure leaves behind, which Kotlin discourages.

solid answer

~40 s

All three relate to the erased runtime `List`, but they're distinct at the type level. **`List<*>`** is a **star projection**: "a `List<out Any?>` of some specific but unknown element type." You can read elements (typed `Any?`) but cannot call producer-unsafe operations with concrete arguments. **`List<Any?>`** is a concrete parameterization whose element type genuinely *is* `Any?`; a `List<String>` is *not* assignable to `MutableList<Any?>` (invariance) but *is* to `List<*>`. The **raw `List`** is the Java-side untyped type that erasure produces; in Kotlin you don't write raw types, but interop and reflection expose them, and Kotlin maps unknown-argument situations to `List<*>`. The star projection is Kotlin's type-safe stand-in for what the runtime actually knows after erasure.

code

kotlin · 9 lines
kotlin
val a: List<String> = listOf("x")
val star: List<*> = a      // unknown-argument view
val anyL: List<Any?> = a   // List is covariant, so this is fine

fun count(list: List<*>) = list.size  // accepts any List
fun addBad(ml: MutableList<*>) {
    // ml.add("x") // compile error: can't add to a star-projected mutable list
    ml.clear()     // ok
}

go deeper

for a junior

Knows List<*> means 'a list of something' and you read it as Any?.

for a middle

Distinguishes List<*> from List<Any?> and knows you can't add to a star-projected mutable list.

for a senior

Explains covariance, the raw-type/interop mapping, and how star projection is the safe runtime-honest view after erasure.

for a principal

Reasons about variance and projections in public API signatures and interop boundaries, choosing List<*> vs List<Any?> deliberately.

## Why this distinction matters Erasure leaves a single runtime `List`. But Kotlin's *type system* offers several ways to talk about "a list whose argument I don't (fully) specify," and conflating them causes real bugs. ## `List<*>` — star projection `List<*>` means **"`List` of some unknown but fixed type."** For a declaration-site `out`/`in`-free interface, it behaves like `List<out Any?>`: - You can **read**: elements come out typed `Any?`. - For a `MutableList<*>`, you **cannot add** a concrete element (except `null` where allowed), because the real element type is unknown and might be narrower than what you'd pass. ```kotlin fun sizeOf(list: List<*>) = list.size // accepts List<String>, List<Int>, ... fun broken(ml: MutableList<*>) { // ml.add("x") // error: argument type is the unknown projection ml.removeAt(0) // ok: doesn't need the element type } ``` ## `List<Any?>` — a concrete parameterization Here the element type **is** `Any?`. It is a fully specified type, not a projection: - `List<String>` **is** assignable to `List<*>` (any list). - `List<String>` is **not** assignable to `MutableList<Any?>` (that would let you add a non-`String`); read-only `List` is covariant (`List<out E>`), so `List<String>` *is* assignable to `List<Any?>`. - `MutableList<Any?>` lets you add anything. ## Raw `List` — the Java/erasure view A **raw type** is `List` with *no* argument, a Java legacy form that erasure underlies. Kotlin **doesn't let you declare** raw types; when Kotlin meets one (from Java interop or reflection) it treats it as a platform/`(Mutable)List<*>!`-like type. Using raw types in Java defeats generic safety; Kotlin's `List<*>` is the *safe* counterpart that preserves read typing as `Any?`. ## Relationship summary - Runtime (after erasure): all are the same raw `List` class. - Compile time: `List<*>` = unknown arg (safe, read-only-ish), `List<Any?>` = arg is exactly `Any?`, raw `List` = no arg (unsafe, Java-only). - Kotlin steers you to `List<*>` whenever you'd be tempted to use a raw type. ```kotlin val a: List<String> = listOf("x") val star: List<*> = a // ok: unknown-arg view val anyL: List<Any?> = a // ok: List is covariant in its element // val mut: MutableList<Any?> = mutableListOf("x") as MutableList<Any?> // would be unsafe ```

  • Why can you read from a `MutableList<*>` but not add a concrete value to it?
    The star projection treats it as a producer (`out Any?`) for reads, so reads yield `Any?` safely. Adding requires knowing the exact element type — the real type might be narrower (e.g. `String`) — so the compiler forbids passing a concrete argument to its `add`.
  • Is `List<String>` assignable to `List<Any?>` in Kotlin?
    Yes, because the read-only `List<out E>` is covariant in `E`. It is *not* assignable to `MutableList<Any?>`, which is invariant, since that would allow adding non-Strings.

saying these in an interview costs you the question

  • Treating `List<*>` and `List<Any?>` as interchangeable
  • Thinking you can `add` a concrete element to `MutableList<*>`
  • Claiming Kotlin lets you declare raw types like Java
  • Saying `List<String>` is assignable to `MutableList<Any?>`
  • Ignoring covariance of read-only `List` when comparing the three

context