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?
answer
- Arrays covariant: String[] IS-A Object[] -> ArrayStoreException at runtime
- Generics invariant: List<String> is NOT List<Object> -> compile error
- Erasure removes generic types at runtime -> safety must be compile-time
- Arrays are reified (carry element type); generics are erased
- Wildcards (? extends / ? super, PECS) reintroduce safe variance
basics
~20 sArrays 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 sArray 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// 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 compilergo deeper
May not know the terms; can at least recognize that arrays throw ArrayStoreException at runtime while the generic version fails to compile.
Defines covariance vs invariance, gives the canonical example, and names ArrayStoreException; understands the bug is caught earlier with generics.
Explains erasure as the root cause of invariance and reification as why arrays do runtime checks, and uses PECS wildcards to reintroduce safe variance.
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>