Walk through how the compiler keeps Array<out T> sound: what type does it assign to set's parameter, what is the type of get under Array<in T>, and how does this preserve type safety?
answer
- out → set param projected to Nothing (uncallable)
- in → get return projected to Any? (imprecise)
- Nothing = bottom, no instances; Any? = top
- read at supertype, write at subtype of all elements
- purely static, zero runtime cost
basics
~20 sUnder Array<out T> the compiler types set's value as Nothing, so no value can be written. Under Array<in T> it types get's result as Any?, so reads are imprecise. Both rules block the operations that could store the wrong type.
solid answer
~50 sProjection soundness is enforced by rewriting member signatures at the use site. For Array<out T>, any member where T is an in-position parameter has that parameter projected to Nothing — the bottom type with no instances — so set(index, value: Nothing) is uncallable; you can never produce a Nothing to pass. For Array<in T>, any member where T is an out-position return has that result projected to the upper bound, effectively Any? for an unbounded T, so get(index): Any? loses precision but stays callable. The invariant being preserved: you may only read at a type you can prove is a supertype of every possible element (out → read T), and you may only write a value you can prove is a subtype of every possible element (in → write T). Nothing and Any? are the bottom/top types that make these bounds explicit. The checks are purely static; the runtime array object is unchanged, so there is no projection cost at runtime.
code
kotlin · 9 linesfun out(a: Array<out Number>) {
val n: Number = a[0] // get(): Number
// a[0] = 1 // set(value: Nothing) -> error
}
fun cin(a: Array<in Number>) {
a[0] = 1 // set(value: Number) -> ok
val x: Any? = a[0] // get(): Any?
}go deeper
Can say out blocks set and in makes reads imprecise, without the type names.
Names Nothing for set under out and Any? for get under in.
Explains the supertype/subtype-of-all-elements invariant and that it is static with no runtime cost.
Reasons in terms of unknown element type S and projecting to lower/upper bounds, including the bounded-T generalization.
## The soundness goal A projection must guarantee that, whatever the **real** element type of the array is, every operation you are allowed to perform is type-safe. The compiler achieves this by **projecting member signatures**: it substitutes the bottom or top type for `T` in the positions that would otherwise be unsound. ## Two foundational types - **`Nothing`** — the **bottom type**. It is a subtype of every type and has **no values**. You can never construct a `Nothing`. - **`Any?`** — effectively the **top type** (nullable `Any`); every value is an `Any?`. ## Out-projection: Array<out T> The real array is `Array<S>` for some unknown `S <: T`. - `get(index): T` — sound. Whatever `S` is, an `S` is a `T`, so reading at type `T` is safe. Kept as `get(): T`. - `set(index, value: T)` — **unsound** if `value: T` were allowed: you might write a `T` that is not an `S` into an `Array<S>`. The compiler projects the parameter to the **lower bound of all possible `S`**, which is `Nothing`. Signature becomes `set(index, value: Nothing)`. Since no value has type `Nothing`, `set` is statically uncallable. ```kotlin fun f(a: Array<out Number>) { val n: Number = a[0] // get: Number — fine // a[0] = 1 // set(value: Nothing) — no argument can satisfy it } ``` ## In-projection: Array<in T> The real array is `Array<S>` for some unknown `S >: T`. - `set(index, value: T)` — sound. Any `S` array can hold a `T`, because `T <: S`. Kept as `set(value: T)`. - `get(index): T` — **imprecise** if returned as `T`: the element is really an `S`, which is broader than `T`. The compiler projects the return to the **upper bound of all possible `S`** — for an unbounded `T` that is `Any?`. Signature becomes `get(): Any?`. ```kotlin fun g(a: Array<in Number>) { a[0] = 1 // set(value: Number) — fine val x = a[0] // get(): Any? — imprecise but safe } ``` ## The two invariants in one sentence - **Read only at a type that is a supertype of every possible element** → `out` keeps `get(): T`, blocks writes (`Nothing`). - **Write only a value that is a subtype of every possible element** → `in` keeps `set(value: T)`, widens reads (`Any?`). `Nothing` and `Any?` are exactly the bounds that make "every possible element" precise. ## No runtime cost All of this is **compile-time**. The underlying array object and its bytecode are untouched; projection adds no boxing, wrappers, or checks at runtime. It is a static restriction on which calls type-check. ## Bounded T If `T` has an upper bound, e.g. `<T : Number>`, then under `Array<in T>` `get` is projected to that bound's nullability-aware type rather than `Any?` in the general unbounded case — the principle (project to the bound) is the same; `Any?` is just the bound when `T` is unbounded.
- If T is declared <T : CharSequence>, what does get return under Array<in T>?It is projected to the type parameter's upper bound (CharSequence, made nullable as needed) rather than the unbounded Any?, because every possible element is at least that bound.
- Does any of this add runtime overhead?No. Projection is entirely a compile-time type-checking mechanism; the generated bytecode for the array is identical.
Nothing is a lock with no key (you can never open set); Any? is a frosted window (you can look but only see a vague shape).
saying these in an interview costs you the question
- Saying set throws at runtime rather than being projected to Nothing statically
- Claiming get under in returns the element's real type
- Confusing Nothing with null or Unit
- Asserting projection boxes or wraps the array at runtime
- Not connecting the bounds to the unknown real element type S