Generics are invariant in Java
List<Integer> is not a List<Number>, unlike arrays.
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>(); // ✓Your turn
What happens?
List<String> strs = new ArrayList<>();
List<Object> objs = strs;
objs.add(42);Runs fineCompile errorThrows 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 IntegerThe runtime check
What happens?
Number[] nums = new Integer[2];
nums[0] = 1;
nums[1] = 2.5;
System.out.println(nums[1]);Prints 2.5Compile errorThrows 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.
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); // ✓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
- List<Integer> → List<Number>: compile error
- Integer[] → Number[]: allowed, but risky
- Bad array stores fail at runtime with ArrayStoreException
- 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.
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);- Runs fine
- Compile error
- Throws ClassCastException
- 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]);- 42
- Compile error
- Throws ArrayStoreException
- 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.