skip to content

What runtime mechanism makes covariant array stores safe, and what are its costs and design implications?

level: principalimportance: nice to knowfreq 30%

answer

  1. aastore bytecode = runtime store check; iastore/etc. have none
  2. Arrays reified (carry component type); generics erased
  3. Check is a soundness patch for covariance, not a feature
  4. JIT can elide/hoist the check for exact, monomorphic stores
  5. Avoid cost: exact-typed arrays, no Object[] aliasing, or primitive arrays

basics

~20 s

The JVM checks every store into a reference array against the array's real element type, throwing ArrayStoreException on a mismatch. This costs a small per-store check and is part of why invariant generics were preferred for new APIs.

solid answer

~50 s

Covariant arrays are type-unsound, so the JVM restores safety dynamically: arrays are reified (each array object carries its real component type), and the aastore bytecode performs a runtime assignability check on every reference store, throwing ArrayStoreException when the value doesn't fit. The cost is a per-store type check on the write path of reference arrays (primitive arrays have no such check). In practice the JIT can often elide or hoist the check when it can prove the stored type matches — for example monomorphic loops over an array of known exact type — but in polymorphic or aliased code it remains. The design implications are significant: the check is a soundness patch, not a feature, and it pushed later language design toward compile-time-safe invariant generics with erasure (no runtime check, but no reified element type either). For latency-sensitive write loops, using exact-typed arrays, avoiding wide Object[] aliasing, or using primitive arrays sidesteps it.

go deeper

for a junior

Aware that storing the wrong type into an array fails at runtime; not expected to know the bytecode mechanism.

for a middle

Knows the JVM checks reference-array stores and that primitives don't pay this cost.

for a senior

Explains reification vs erasure, the aastore store check, and basic performance/design implications.

for a principal

Reasons about type-system soundness, JIT check elision, and translates the mechanism into concrete API and hot-path performance guidance and cross-feature design rationale.

## The soundness gap and how the JVM fills it Covariant arrays make Java's static type system **unsound** for array writes: the compiler will let you store any `Object` through an `Object[]` reference that actually points at a `String[]`. A sound type system would never let a program reach a state that violates its types; covariant arrays can. Java accepts the unsoundness statically and **repairs it dynamically**. Two ingredients make this possible: 1. **Reification.** Unlike generics (erased), arrays are **reified** — every array object stores its real **component type** in its header/class metadata at runtime. A `String[]` knows it is a `String[]` even when viewed through an `Object[]` variable. 2. **A checked store bytecode.** Storing into a reference array compiles to the **`aastore`** JVM instruction. Its specification requires a runtime check: the value being stored must be assignment-compatible with the array's actual component type. If not, `aastore` throws **`ArrayStoreException`**. (Primitive stores use `iastore`, `dastore`, etc., which have no such check — there is no covariance to police.) So *every reference-array element write* carries an implicit `instanceof`-like check against the array's reified component type. ## The costs - **Per-store CPU cost.** Each `aastore` is more than a memory write: a type comparison. In tight write loops over large reference arrays this is measurable. - **Optimization friction.** The check inhibits some naive optimizations because the compiler must preserve the possible exception. - **JIT mitigations.** Modern JITs (HotSpot C2, etc.) frequently **eliminate or hoist** the store check when they can prove the array's exact type and that all stored values fit — e.g., a loop filling a freshly allocated `String[]` with Strings. The check survives where the array is **aliased through a wider static type** (`Object[]`), where the element type is polymorphic, or where type analysis can't prove safety. So the cost is workload-dependent, often near-zero in monomorphic code and real in polymorphic/aliased code. ## Design implications (the principal-level view) - **The check is a patch, not a feature.** It exists only because covariance is unsound. Recognizing that reframes "why arrays behave oddly": the language traded static soundness for pre-generics reusability and paid for it with runtime checks. - **It shaped generics.** Generics were intentionally made **invariant + erased**: invariance gives compile-time safety (no need for a runtime check), and erasure means there's no element type to check anyway. The flip side — no generic array creation, heap-pollution warnings — stems directly from the arrays-are-reified-and-covariant vs generics-are-erased-and-invariant mismatch. - **API and performance guidance.** Prefer `List<T>` to `T[]` for safety; when arrays are necessary on a hot path, keep them **exact-typed** (don't alias `String[]` as `Object[]`), or use **primitive arrays** which avoid the store check entirely. Bulk operations like `System.arraycopy` between compatible exact types avoid per-element surprises; copying into a wider array can surface `ArrayStoreException`. - **Reflection.** `Array.newInstance(componentType, len)` plus reflective set obeys the same store check; libraries building arrays of a runtime-chosen type must handle `ArrayStoreException`. ## Mental model Think of a covariant array reference as a *view* that may be wider than the object. The object never forgets its true element type; the JVM consults that truth on every write and refuses writes that would corrupt it. Generics, by contrast, forget the element type entirely at runtime and instead refuse the dangerous code at compile time.

  • Why can't the JVM do the same kind of store check for generic collections?
    Generics are erased: at runtime a List<String> is just a List with no element-type metadata. There is nothing for a store check to compare against, so safety must instead be guaranteed at compile time via invariance.
  • When can the JIT remove the array store check?
    When it can prove the array's exact component type and that every stored value is assignment-compatible — e.g., a loop filling a newly allocated, exactly-typed array with values of that type, with no aliasing through a wider reference. Polymorphic stores or wide Object[] aliasing keep the check.

saying these in an interview costs you the question

  • Claiming the store check is free or negligible in all cases
  • Saying generics get the same runtime store check (they're erased)
  • Believing primitive array stores do a covariance check
  • Asserting the JIT always eliminates the check (only when it can prove safety)

context