skip to content

For a class with a recursive (F-bounded) parameter like `class Node<T : Comparable<T>>`, what does `Node<*>` read at, and what subtle limitations does star projection impose here?

level: principalimportance: nice to knowfreq 18%

answer

  1. Recursive bound T:Comparable<T> -> read at Comparable<*>
  2. Each star = a distinct, unrelated unknown type
  3. Two Node<*> can't be compared to each other
  4. Capture <T:Comparable<T>> to relate two values
  5. Single-value ops may work via type-argument capture

basics

~20 s

With a self-referential bound like T : Comparable<T>, Node<*> projects T to that bound, but the bound mentions T again, so it's star-projected too: you read at Comparable<*>. You can read generically but can't safely compare two different starred nodes.

solid answer

~40 s

When the upper bound itself references the type parameter (an **F-bound** / recursive bound), star projection substitutes the bound but must recursively project its own occurrence of `T`. For `class Node<T : Comparable<T>>`, `Node<*>` reads its `T` at `Comparable<*>` (not `Comparable<Any?>` and not a concrete type). The practical limitation: you cannot use the recursive capability across two independently-starred values. Two values `a: Node<*>` and `b: Node<*>` have *different unknown* `T`s, so `a.value.compareTo(b.value)` won't type-check — each star is a distinct existential. To regain the relation you must capture a real type parameter: `fun <T : Comparable<T>> compare(a: Node<T>, b: Node<T>)`. Star is fine for reading or single-element operations but forgets that two starred instances might share a type.

code

kotlin · 14 lines
kotlin
class Node<T : Comparable<T>>(val value: T)

// star: read-only, can't relate two nodes
fun label(n: Node<*>): String = n.value.toString()

// captured T: can relate, compare, sort
fun <T : Comparable<T>> maxNode(a: Node<T>, b: Node<T>): Node<T> =
    if (a.value >= b.value) a else b

fun main() {
    val x = Node(3); val y = Node(7)
    println(label(x))                // "3"
    println(maxNode(x, y).value)     // 7
}

go deeper

for a junior

Recognizes Node<*> means unknown element and that you can read it generically.

for a middle

Knows star reads at the bound and that you generally can't write or compare freely.

for a senior

Explains that two stars are distinct unknowns and switches to a captured <T> to compare.

for a principal

Articulates F-bounds, recursive projection to Comparable<*>, type-argument capture, and the existential-per-star semantics.

## F-bounded (recursive) bounds A **recursive** or **F-bounded** type parameter is one whose upper bound mentions the parameter itself: `T : Comparable<T>`. It's the idiom for 'a type comparable to its own kind'. ## How star projects a recursive bound Star projection replaces the unknown `T` with its declared upper bound. But that bound, `Comparable<T>`, *contains* `T`. The compiler can't leave a free `T`, so it **recursively star-projects** the inner occurrence: - `Node<*>` reads its `T` as `Comparable<*>` — i.e., 'comparable to some unknown type', not `Comparable<Any?>` and not a concrete element type. ```kotlin class Node<T : Comparable<T>>(val value: T) fun peek(n: Node<*>) { val v: Comparable<*> = n.value // read type is Comparable<*> println(v) } ``` ## The key limitation: each star is a distinct existential Every star projection introduces its **own** fresh unknown type. Two separately-starred values are *not* known to share a type: ```kotlin fun broken(a: Node<*>, b: Node<*>) { // a.value.compareTo(b.value) // ERROR // a.value : Comparable<*> ; compareTo expects the SAME unknown type as a's T, // but b.value's unknown type is unrelated. } ``` This is fundamental: `*` *forgets* the type, so it can't reconnect two values. The recursive bound is exactly the case where you'd *want* to relate them, which is why star is so limiting here. ## Restoring the relation with a captured type parameter To compare two nodes, name a single `T` so both share it: ```kotlin fun <T : Comparable<T>> compare(a: Node<T>, b: Node<T>): Int = a.value.compareTo(b.value) // both T -> compareTo is well-typed ``` Here the type parameter *captures* the shared type, which `*` cannot. ## Self-operation within one starred value You can sometimes operate on a single starred value because the compiler captures its one unknown type locally (type-argument capture). With *two* stars there are two independent unknowns, so cross-comparison fails. ## Design takeaways - Use `Node<*>` for read-only/diagnostic access where you never relate two instances. - The moment you need to compare, sort, merge, or otherwise *relate* two values of the recursive type, switch to a captured `<T : Comparable<T>>` parameter. - This generalizes: any operation requiring two values to share the unknown type cannot be expressed with independent star projections — that's the defining boundary of `*` versus a named type variable.

  • Why is the read type `Comparable<*>` rather than `Comparable<Any?>`?
    Because the bound `Comparable<T>` references the very parameter being eliminated; the compiler recursively star-projects that inner `T`, yielding `Comparable<*>`. `Comparable<Any?>` would falsely claim it's comparable to anything.
  • Can you sort a `List<Node<*>>` by natural order?
    Not with `compareTo`, because each element's unknown `T` is independent, so comparing two elements doesn't type-check. You'd need a uniform `Comparator` on a type-erased key, or constrain to a shared `<T : Comparable<T>>`.

Two sealed boxes each labeled 'comparable to its own contents' — you can't compare box A's contents to box B's, because nothing guarantees they hold the same kind of thing; you'd need both stamped with the same serial number (a shared T).

saying these in an interview costs you the question

  • Claiming `Node<*>` reads at `Comparable<Any?>` or `Any?`
  • Thinking two `Node<*>` values can be compared directly
  • Not recognizing each star is a separate existential type
  • Believing a captured `<T>` and `*` are interchangeable here
  • Overlooking that single-value operations may still type-check

context