🧪 Generics · Intermediate

Generics are invariant in Java

List<Integer> is not a List<Number>, unlike arrays.

🧩 The mysteryInteger is a Number. Integer[] is a Number[]. But List<Integer> is NOT a List<Number>. Is Java being inconsistent, or is it saving you from something?

A thought experiment

Suppose Java let you treat a List<Integer> as a List<Number>. Through that reference you could add a Double, and the list of Integers would be corrupted. Someone reading an Integer later would crash.

List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;   // imagine...
nums.add(3.14);             // a Double!
Integer i = ints.get(0);    // 💥

Generics are invariant

So Java forbids it: **List<A> and List<B> are unrelated unless A and B are exactly the same, even when Integer extends Number. Need flexibility? A wildcard like List<?>** or List<? extends Number> explicitly allows it (and restricts adding).

List<Object> a = new ArrayList<String>(); // ✗
List<?> b = new ArrayList<String>();      // ✓
List<String> c = new ArrayList<String>(); // ✓
🔮 Predict it

Your turn

What happens?

List<String> strs = new ArrayList<>();
List<Object> objs = strs;
objs.add(42);
  1. Runs fine
  2. Compile error
  3. Throws ClassCastException
Show the answer

**List<Object> objs = strs is rejected.** That's exactly what stops the next line from sneaking an Integer into a list of Strings.

Arrays play by older rules

Arrays are covariant: an Integer[] is a Number[], so the assignment compiles. To stay safe, every array remembers its real element type and checks each store at runtime.

Number[] nums = new Integer[2]; // compiles
nums[0] = 1;   // fine, an Integer
🔮 Predict it

The runtime check

What happens?

Number[] nums = new Integer[2];
nums[0] = 1;
nums[1] = 2.5;
System.out.println(nums[1]);
  1. Prints 2.5
  2. Compile error
  3. Throws ArrayStoreException
Show the answer

It compiles because arrays are covariant. But the array knows it's really an Integer[], so storing a Double fails at runtime with **ArrayStoreException**.

Why the double standard?

An array object carries its element type at runtime, so it can check every store. A List<Integer> at runtime is just a List (the type argument is erased), so it can't check anything. The only safe place to check is compile time, which means invariance.

⚠️ The trap

List<Object> isn't "any list"

A method taking List<Object> won't accept a List<String>. If you just want to read any list, use **List<?>**.

List<String> names = List.of("a");
static void printAll(List<Object> xs) { }
printAll(names);   // ✗ compile error
 
static void printAll2(List<?> xs) { }
printAll2(names);  // ✓
💼 In the real world

Interview favourite

"Why are arrays covariant but generics invariant?" is a classic senior-level question. The answer: arrays are reified and checked at runtime (ArrayStoreException), while generics are erased, so the compiler must block unsafe assignments up front.

Key takeaways

  1. List<Integer> → List<Number>: compile error
  2. Integer[] → Number[]: allowed, but risky
  3. Bad array stores fail at runtime with ArrayStoreException
  4. Need flexibility? Use wildcards like List<? extends Number>

💡 A basket of apples is not a 'basket of fruit' you can drop bananas into — even though apples are fruit.

🤯 Did you know?

Before generics existed, covariant arrays let a single method like Arrays.sort(Object[]) sort a String[] or an Integer[]. The price was a runtime check on every array store.

Practice questions

What happens with this code?

List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;
nums.add(3.14);
  1. Runs fine
  2. Compile error
  3. Throws ClassCastException
  4. Throws ArrayStoreException
Check your answer

Compile error. The assignment List<Number> nums = ints is rejected. That's exactly what prevents the next line from sneaking a Double into a list of Integers.

What happens when this runs?

Object[] arr = new String[2];
arr[0] = "ok";
arr[1] = 42;
System.out.println(arr[1]);
  1. 42
  2. Compile error
  3. Throws ArrayStoreException
  4. Throws ClassCastException
Check your answer

Throws ArrayStoreException. Arrays are covariant, so a String[] can be referenced as Object[] and the code compiles. But the array remembers it's a String[] and rejects the Integer at runtime.

Next: wildcards. What does ? really mean, and why can't you add 5 to a List<? extends Number>?