skip to content

Explain array variance in Kotlin: why is Array<T> invariant, and how do primitive arrays and varargs relate to Java's covariant arrays?

level: principalimportance: nice to knowfreq 30%

answer

  1. Array<T> is invariant; Java arrays are covariant
  2. Java covariance -> runtime ArrayStoreException
  3. Array<out T> = read-only covariant projection
  4. primitive arrays are non-generic, no variance
  5. vararg is an array; spread with *

basics

~10 s

In Kotlin you cannot pass an Array<String> where an Array<Any> is expected — arrays are invariant. This prevents a class of runtime errors that Java's covariant arrays allow.

solid answer

~40 s

Kotlin's Array<T> is INVARIANT: Array<String> is not a subtype of Array<Any>, so you can't substitute one for the other. Java arrays are covariant (String[] IS an Object[]), which lets you write into them with the wrong type and blow up at runtime with ArrayStoreException. Kotlin closes that hole at compile time by making Array invariant; if you only read, you can opt into covariance at a call site with Array<out T>. Primitive arrays (IntArray etc.) are separate non-generic classes, so variance doesn't apply. For Java interop, Kotlin maps Array<out T> to T[] when needed. Practically, varargs interplay: a vararg parameter is an array; you spread an existing array with the * operator, and the platform handles the boxing/primitive distinction. The invariance is a deliberate safety win that occasionally forces explicit out projections or copies.

code

kotlin · 8 lines
kotlin
fun printAll(items: Array<out Any>) = items.forEach(::println)

val strings: Array<String> = arrayOf("a", "b")
// val anys: Array<Any> = strings   // error: invariant
printAll(strings)                    // OK via Array<out Any> projection

fun log(vararg m: String) = m.forEach(::println)
log(*strings)                        // spread operator forwards the array

go deeper

for a junior

May simply know you can't assign Array<String> to Array<Any> without grasping why.

for a middle

Explains invariance and that you use Array<out T> for read-only covariance.

for a senior

Contrasts with Java's covariant arrays and ArrayStoreException, and connects varargs/spread to arrays.

for a principal

Frames invariance as a soundness decision, advises List-based APIs, and reasons about interop, projections, and where the safety trade-offs land in API design.

## Variance, briefly **Variance** is about subtyping of generic types. If `Dog` is a `Animal`, is `Container<Dog>` a `Container<Animal>`? - **Covariant**: yes (`out`). - **Contravariant**: reversed (`in`). - **Invariant**: no relationship. ## Kotlin arrays are invariant `Array<T>` is **invariant**: `Array<String>` is **not** a subtype of `Array<Any>`. ```kotlin val strings: Array<String> = arrayOf("a", "b") // val anys: Array<Any> = strings // compile error — invariant ``` ## Why: Java's covariant-array hole Java arrays are **covariant**: `String[]` *is an* `Object[]`. That seems convenient but is unsound for writes: ```java // Java String[] s = {"a"}; Object[] o = s; // allowed (covariant) o[0] = Integer.valueOf(1); // compiles, but throws ArrayStoreException at runtime ``` The error surfaces only at **runtime** (`ArrayStoreException`). Kotlin makes `Array<T>` invariant so this mistake is a **compile-time** error instead — a deliberate soundness improvement. ## Opting into covariance with `out` If a function only **reads** from an array, you can accept a covariant projection with **`Array<out T>`** (use-site variance / a *projection*): ```kotlin fun printAll(items: Array<out Any>) { for (x in items) println(x) } printAll(arrayOf("a", "b")) // Array<String> accepted via out-projection ``` With `out` you may read elements as `T` but cannot `set` an arbitrary `T` in — exactly the restriction that keeps it safe. ## Primitive arrays sidestep variance `IntArray`, `DoubleArray`, etc. are **non-generic** classes (`int[]`, …). Since there is no type parameter, variance simply doesn't apply — there is no `IntArray` vs `LongArray` subtyping question. ## Java interop mapping Kotlin maps array types to Java arrays for interop. A Kotlin `Array<out T>` (or a platform-typed array coming from Java) corresponds to `T[]`; primitive arrays map to the native `int[]`, `byte[]`, etc. Java's covariance still exists on the Java side, so a returned `Object[]` may carry the usual caveats as a platform type. ## Varargs connection A `vararg` parameter **is an array** under the hood (`vararg xs: Int` is an `IntArray`; `vararg xs: T` is an `Array<out T>`). To forward an existing array into a vararg you use the **spread operator** `*`: ```kotlin fun log(vararg msgs: String) { msgs.forEach(::println) } val arr = arrayOf("x", "y") log(*arr) // spread; without * it would be a single Array argument ``` Note the vararg of `T` is declared `out`, which is why you can pass a more specific array. ## Takeaways for design - Invariance is a **safety feature**; embrace it and use `out` projections where read-only. - Prefer `List`/`MutableList` in APIs unless you need array semantics; collection variance (`List<out T>`) is more ergonomic. - Reserve raw arrays for interop, varargs, and primitive performance.

  • What runtime error does Java's array covariance risk, and how does Kotlin avoid it?
    ArrayStoreException on a bad write. Kotlin makes Array<T> invariant, turning the mistake into a compile-time error.
  • Why don't IntArray and LongArray have variance concerns?
    They are non-generic classes (int[], long[]) with no type parameter, so there is no subtyping relationship to be variant over.
  • What does Array<out T> let you do and not do?
    Read elements as T (covariant), but it forbids setting an arbitrary T, which is what makes the projection safe.

Java's covariant arrays are an unlocked door labelled 'subtypes welcome' that lets the wrong type sneak in and crash at runtime; Kotlin's invariance keeps the door locked until you explicitly mark it read-only with out.

saying these in an interview costs you the question

  • Saying Array<String> is assignable to Array<Any> in Kotlin
  • Not knowing Java arrays are covariant or about ArrayStoreException
  • Confusing declaration-site and use-site (out projection) variance
  • Claiming primitive arrays have variance rules
  • Forgetting that a vararg parameter is itself an array

context