skip to content

Explain array covariance versus generics invariance: why does Object[] arr = new String[1]; arr[0] = 1; compile but throw at runtime, while the List equivalent won't compile?

level: seniorimportance: should knowfreq 45%

answer

  1. Arrays covariant: String[] IS-A Object[] -> ArrayStoreException at runtime
  2. Generics invariant: List<String> is NOT List<Object> -> compile error
  3. Erasure removes generic types at runtime -> safety must be compile-time
  4. Arrays are reified (carry element type); generics are erased
  5. Wildcards (? extends / ? super, PECS) reintroduce safe variance

basics

~20 s

Arrays are covariant: a String[] can be used as an Object[], so storing the wrong type compiles but fails at runtime with ArrayStoreException. Generics are invariant: a List<String> is not a List<Object>, so the bad assignment is caught by the compiler instead.

solid answer

~50 s

Array covariance means String[] is considered a subtype of Object[]. That lets you assign Object[] arr = new String[1]; the compiler allows arr[0] = someInt because the static type is Object[], but the array remembers its real element type at runtime and throws ArrayStoreException on the bad store. So arrays trade compile-time safety for runtime checks. Generics are invariant: List<String> is NOT a subtype of List<Object>, so List<Object> l = new ArrayList<String>(); simply won't compile, catching the error early. This invariance exists because of type erasure — generic type info is gone at runtime, so the JVM couldn't perform an ArrayStoreException-style check, hence the safety must be enforced at compile time. The practical upshot: generics give stronger, earlier guarantees, which is one more reason to prefer collections over arrays, and to use wildcards (? extends/super) when you do need covariance or contravariance.

code

java · 11 lines
java
// Arrays: covariant -> compiles, fails at runtime
Object[] arr = new String[1];
arr[0] = Integer.valueOf(1); // ArrayStoreException at runtime

// Generics: invariant -> caught by the compiler
// List<Object> l = new ArrayList<String>(); // does NOT compile

// Safe covariance via wildcards (PECS: Producer Extends)
List<? extends Number> nums = new ArrayList<Integer>();
Number n = nums.get(0); // reading is fine
// nums.add(2);          // adding is rejected by the compiler

go deeper

for a junior

May not know the terms; can at least recognize that arrays throw ArrayStoreException at runtime while the generic version fails to compile.

for a middle

Defines covariance vs invariance, gives the canonical example, and names ArrayStoreException; understands the bug is caught earlier with generics.

for a senior

Explains erasure as the root cause of invariance and reification as why arrays do runtime checks, and uses PECS wildcards to reintroduce safe variance.

for a principal

Articulates the language-design trade-off (early covariance decision before generics), guides API design toward Lists and proper wildcard signatures, and reasons about how reification gaps affect library/framework design (e.g. generic factories, type tokens).

## The two concepts: subtyping and variance First, **subtyping**: `String` is a subtype of `Object` — every String *is an* Object. **Variance** is the question of what that implies for *containers* of those types. If `String` is a subtype of `Object`, is `String[]` a subtype of `Object[]`? Is `List<String>` a subtype of `List<Object>`? Java answers these two questions **differently**, and that difference is the whole topic. - **Covariant**: the container subtyping follows the element subtyping (String[] *is a* Object[]). - **Invariant**: it does not (List<String> is *not* a List<Object>). ## Arrays are covariant Java decided long ago that **arrays are covariant**: because String is a subtype of Object, `String[]` is treated as a subtype of `Object[]`. So this compiles: ```java Object[] arr = new String[1]; // legal: String[] IS-A Object[] arr[0] = "hello"; // fine arr[0] = Integer.valueOf(1); // compiles! but... ``` The third line **compiles** because, to the compiler, `arr` has the static type `Object[]`, and an `Integer` is an `Object`. But the *actual* array on the heap is a `String[]`, and it **remembers its true element type at runtime**. So when you try to store an Integer into it, the JVM checks and throws a runtime exception: ``` java.lang.ArrayStoreException: java.lang.Integer ``` This is the price of covariance: arrays accept some illegal stores at compile time and catch them only at runtime with **ArrayStoreException**. Every array write carries a small runtime type check to enable this. ## Generics are invariant Generics took the opposite, stricter path: they are **invariant**. `List<String>` is **not** a subtype of `List<Object>`, even though String is a subtype of Object. So the analogous code **does not even compile**: ```java List<Object> l = new ArrayList<String>(); // COMPILE ERROR ``` The error is caught by the compiler — the earliest, cheapest place to catch a bug. Had Java allowed it, you could then do `l.add(1)` and corrupt a list someone thinks is all Strings, with no runtime guard to stop you. ## Why the difference? Type erasure Why can't generics behave like arrays and just check at runtime? Because of **type erasure**. Generic type parameters exist only at compile time; the compiler uses them to check your code, then **erases** them. At runtime an `ArrayList<String>` and an `ArrayList<Integer>` are both just `ArrayList` — the `<String>` is gone. So the JVM has **no runtime element type to check against**; there is no information to throw an "ListStoreException" with. Therefore the only place generic type safety can live is the **compiler**, which is exactly why generics must be invariant: invariance lets the compiler prove safety without any runtime type tag. Arrays, by contrast, are **reified** — they *do* carry their element type at runtime — which is precisely what makes the runtime ArrayStoreException check possible. ## Getting covariance back safely: wildcards Invariance is sometimes inconvenient. Generics offer **bounded wildcards** to opt into variance safely: ```java List<? extends Number> nums = new ArrayList<Integer>(); // covariance: you can READ Numbers // nums.add(1); // but you CANNOT add — compiler forbids it, preserving safety List<? super Integer> sink = new ArrayList<Number>(); // contravariance: you can ADD Integers ``` The mnemonic is **PECS — Producer Extends, Consumer Super**: use `? extends T` when the structure *produces* (you read) T, and `? super T` when it *consumes* (you write) T. Wildcards give the flexibility of covariance without the ArrayStoreException hole, because the compiler restricts the operations that would be unsafe. ## The takeaway Arrays = covariant + reified → flexible assignment but **runtime** failure (ArrayStoreException). Generics = invariant + erased → **compile-time** safety, no runtime surprises. This mismatch is one of the canonical reasons Effective Java advises preferring **Lists over arrays**: the compiler catches your mistakes sooner.

  • Why does generic invariance, combined with erasure, mean you can't create new T[] inside a generic class?
    Because the element type T is erased at runtime, the JVM can't create an array with the correct reified component type, and an array's covariant store-check needs that type. So generic array creation (new T[]) is disallowed; you use an Object[] cast or, better, a List<T>.
  • How do wildcards let you accept a List<String> where a List<Number> won't fit?
    Declare the parameter as List<? extends Number>. That covariant wildcard accepts List<Integer>, List<Double>, etc. You can read elements as Number but cannot add (except null), which is exactly what keeps it type-safe.

saying these in an interview costs you the question

  • Saying List<String> is a subtype of List<Object> (it is not — invariance)
  • Claiming arrays catch the bad store at compile time (it's runtime)
  • Not knowing the exception is ArrayStoreException
  • Forgetting that erasure is the reason generics can't do a runtime check
  • Thinking you can freely add to a List<? extends Number>

context