What does the compiler erase from a generic type, and how do you decide between ? extends and ? super?
Generic type arguments exist only for the compiler; the bytecode keeps a raw type plus a signature string, which is why there is no T.class and no new T[]. Wildcards then follow producer-extends, consumer-super: read through ? extends, write through ? super.
What the interviewer is scoring
- Does the candidate explain erasure as migration compatibility with pre-generics code rather than as an arbitrary limitation
- Whether they know some generic information survives in the class file and can name what reads it back
- That arrays being covariant and reified while generics are invariant and erased is presented as the root of the mismatch
- Can they derive the extends and super rule from what the compiler can prove, instead of reciting the mnemonic
- Whether an unchecked cast is described as deferring the failure rather than removing it
Answer
Erasure buys backwards compatibility
Generics were added in Java 5 to a language that already had a decade of code passing raw List around, and the design constraint was that the old code had to keep working against the new collections. So the type arguments were made a compile-time device. The compiler checks them, then throws most of them away: every type variable is replaced by its leftmost bound, Object if the variable is unbounded, and casts are inserted at the points where you read a value out. ArrayList<String> and ArrayList<Integer> are the same class at runtime, and a raw ArrayList is assignment-compatible with both.
That single decision explains the whole list of things you cannot do. There is no T.class, because at runtime there is no T. You cannot write new T[n], because the JVM would need the component type to build the array. instanceof List<String> will not compile, because the check is unanswerable. You cannot overload two methods on List<String> and List<Integer>, because after erasure both have the signature f(List). A type variable cannot appear in a catch clause, and a static method cannot reference the class's own type parameter, since there is no instance to have supplied it.
The half of this that candidates usually get wrong is claiming that generic information is gone entirely. It is not. The class file keeps a Signature attribute recording the declared generic types of fields, methods, superclasses and interfaces, which is what the compiler reads when it compiles against your jar and what reflection exposes through getGenericSuperclass and getGenericParameterType. What is missing is the type argument of a particular instance. That distinction is why Jackson's TypeReference and Spring's ParameterizedTypeReference work at all: you subclass an abstract generic type, the type argument is baked into the anonymous subclass's declaration, and the library reads it back off the superclass.
Arrays and generics disagree, on purpose
An array is reified and covariant: String[] is a subtype of Object[], and the runtime carries the component type so it can police stores. A generic type is erased and invariant: List<String> is not a subtype of List<Object>, and the runtime carries nothing.
final class Variance {
public static void main(String[] args) {
Object[] array = new String[1];
array[0] = 42; // compiles; throws ArrayStoreException when it runs
// List<Object> list = new ArrayList<String>();
// ^ rejected at compile time instead, because there is no later check to make
}
}
Both rules exist to stop you putting an Integer where a String belongs. Arrays catch it late, at the store, using type information they kept. Generics catch it early, at the assignment, because they will have no type information left to catch it later. Neither is a bug; they are two answers to the same problem, and mixing them is where @SafeVarargs comes from, since a T... parameter has to materialise a T[] that the runtime cannot check.
Deriving PECS rather than memorising it
Invariance is safe but useless on its own, because a method that takes List<Number> cannot be handed a List<Integer>. Wildcards restore the flexibility by admitting exactly what the compiler can still prove.
With List<? extends Number> the element type is some unknown subtype of Number. Reading is fine, because whatever it is, it is a Number. Writing is not, because the compiler cannot prove your Integer belongs in what might be a List<Double>. So ? extends gives you a producer, and the only value you may pass to add is null.
With List<? super Number> the element type is some unknown supertype of Number. Now writing is fine, because a Number fits in any container that holds Number or wider. Reading gives you Object, because that is the only thing every supertype has in common. So ? super gives you a consumer.
import java.util.ArrayList;
import java.util.List;
final class Transfer {
// src produces T, so ? extends. dest consumes T, so ? super.
static <T> void transferAll(List<? extends T> src, List<? super T> dest) {
for (T item : src) {
dest.add(item);
}
// src.add(item); // rejected: the element type is an unknown subtype of T
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
transferAll(ints, numbers);
// Same class object: the type argument left no trace.
System.out.println(ints.getClass() == numbers.getClass()); // false, different impls
System.out.println(new ArrayList<String>().getClass()
== new ArrayList<Integer>().getClass()); // true
}
}
The corollary worth volunteering is that wildcards belong on parameters, not on return types. Returning List<? extends Number> pushes the wildcard into every caller's local variable and buys nothing, since the caller already knows what it asked for.
The unchecked cast is a promise, not a check
The habit that separates a careful answer is what you do when erasure blocks you and you reach for a cast. Casting a raw List to List<String> compiles with an unchecked warning and inserts no runtime check at all, because there is nothing to check. If the list actually holds an Integer, nothing happens at the cast. The ClassCastException fires later, in the caller's code, at the implicit cast the compiler inserted where an element was read — a stack trace pointing at a line that did nothing wrong.
So treat an unchecked cast as an assertion you are making on the compiler's behalf, and confine it: put @SuppressWarnings("unchecked") on the narrowest possible declaration, usually a single local variable, and write the comment explaining why the cast holds. Suppressing it on the whole method silences the next three casts somebody adds. Where you can, prefer Class<T> plus cast() as a type token, which pushes the check back to a point where it can actually be performed.
Likely follow-ups
- Why does the compiler generate a bridge method when you implement Comparable<Foo>?
- What does @SafeVarargs assert, and when would applying it be a lie?
- How does Jackson recover the element type of a List<Order> if erasure removed it?
- Why can you not overload a method on List<String> and List<Integer>?
Related questions
- How do you use TypeScript to make a component's props hard to misuse?mediumAlso on generics4 min
- Your endpoint takes a typed request body and it still crashed on a missing field. How is that possible when it compiled?mediumAlso on type-erasure4 min
- Where does TypeScript stop protecting you, and what do you do at those points?hardAlso on type-erasure4 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: concept6 min
- How do you decide between BFS and DFS - and how do you recognise a graph problem that nobody described as a graph?mediumSame kind of round: coding4 min
- How do goroutines leak, and how would you find a leak in a running service?mediumSame kind of round: concept5 min
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?hardSame kind of round: concept4 min