Generics
Raw types, generic methods, type erasure, bounded type parameters, wildcards, PECS, and heap pollution.
9 questions
MiddleTheoryVery commonWhat is type erasure, and what runtime limits does it impose on generics?
What is type erasure, and what runtime limits does it impose on generics?
Generics exist only at compile time: the compiler erases each type parameter to its bound, or to Object when unbounded, and inserts the casts. So at runtime List<String> and List<Integer> are the identical raw List with no element-type info retained. This forbids new T(), primitive arguments like List<int>, and any runtime check of a generic type such as instanceof List<String>.
Common mistakes
- ✗Assuming a type argument is available at runtime, e.g. expecting
instanceof List<String>to compile - ✗Trying to write
new T()ornew T[], which erasure makes impossible - ✗Overloading two methods that differ only by their generic argument, which collide after erasure
Follow-up questions
- →How does erasure cause a heap-pollution
@SuppressWarnings("unchecked")situation? - →Why must you pass a
Class<T>token to create a generic array at runtime?
JuniorTheoryCommonHow does a generic method's type parameter differ from a generic class's?
How does a generic method's type parameter differ from a generic class's?
A generic method declares its own <T> before the return type, and the compiler infers it fresh on each call. Its class need not be generic at all — that is why Collections.emptyList() works as a static. A class's <T> is bound once per instance, and a static method can never use it.
Common mistakes
- ✗Thinking a generic method requires its enclosing class to be generic
- ✗Expecting a static method to reach the class's own
<T> - ✗Assuming the method's
<T>is bound per instance rather than per call
Follow-up questions
- →Why can a static method declare its own
<T>but never use the class's? - →How does the compiler infer a generic method's type argument at a call site?
JuniorTheoryCommonWhat is a raw List, and how does it differ from List<Object> and List<?>?
What is a raw List, and how does it differ from List<Object> and List<?>?
A raw List is the generic type used with no type argument — a pre-generics leftover. The compiler turns generic checking off for it, so you may add anything and it still compiles, with only an unchecked warning. List<Object> stays fully checked and holds any object. List<?> is checked too, but its element type is unknown, so you may add nothing but null.
Common mistakes
- ✗Believing a raw
ListandList<Object>are interchangeable — only the raw form loses type checking - ✗Trying to
adda non-null element to aList<?>, whose element type is unknown - ✗Ignoring the unchecked warning, which is the compiler saying it can no longer guarantee type safety
Follow-up questions
- →Why does assigning a
List<String>to a rawListdisable generic checks on the whole object? - →Why can you read from a
List<?>but add onlynullto it?
MiddleTheoryCommonWhat does the bound in <T extends Comparable<T>> actually buy you?
What does the bound in <T extends Comparable<T>> actually buy you?
An upper bound lets the compiler call the bounded type's members on T; without it T is only an Object, and it erases to the bound instead of Object. Multiple bounds use & with the class first. A bound constrains what may substitute for T — it is not a runtime check, and only a wildcard, never a type parameter, has a super bound.
Common mistakes
- ✗Thinking an unbounded
Talready lets you callComparablemethods - ✗Believing a bound is checked at run time rather than by the compiler
- ✗Expecting a
superbound on a type parameter, which only a wildcard allows
Follow-up questions
- →Why must
<T extends Number & Comparable<T>>putNumberfirst? - →How does the erased signature change once
Thas an upper bound?
MiddleTheoryCommonHow do List<?>, List<? extends T>, and List<? super T> differ?
How do List<?>, List<? extends T>, and List<? super T> differ?
List<?> is an unbounded wildcard — the element type is unknown, so you may read elements only as Object and add nothing but null. List<? extends T> is an upper-bounded producer: you read elements as T, yet still cannot add any except null, since the exact subtype is unknown. List<? super T> is a lower-bounded consumer: you may add a T or its subtypes, but reads come back only as Object.
Common mistakes
- ✗Trying to
adda non-null element to aList<? extends T>, whose exact subtype is unknown - ✗Expecting to read elements as
Tfrom aList<? super T>, which yields onlyObject - ✗Thinking
List<?>accepts any element like a rawListrather than onlynull
Follow-up questions
- →Why can you read a
TfromList<? extends T>but never add one? - →When would you choose
List<? super T>overList<? extends T>?
MiddleTheoryOccasionalWhat is heap pollution, and why does a generic varargs parameter risk it?
What is heap pollution, and why does a generic varargs parameter risk it?
Heap pollution is a variable of a parameterized type pointing at an object that is not of that type — a List<String> reference actually holding an Integer. Erasure lets it through: the cast is unchecked, so the bad value only surfaces later, at the implicit cast on a read, far from its cause. Generic varargs are the classic source: foo(List<String>... lists) compiles to a reified List[] that array covariance lets you pollute.
Common mistakes
- ✗Confusing heap pollution with a memory leak instead of a broken type invariant
- ✗Expecting a
ClassCastExceptionat the bad store rather than at some later, unrelated read - ✗Putting
@SafeVarargson a method that stores into or returns its varargs array
Follow-up questions
- →Why is a generic varargs parameter reified as an array while its element type is erased?
- →Which methods may carry
@SafeVarargs, and what exactly does the annotation promise?
MiddleTheoryOccasionalHow does the wildcard-choice rule PECS decide between ? extends T and ? super T?
How does the wildcard-choice rule PECS decide between ? extends T and ? super T?
PECS — producer-extends, consumer-super. A parameter that only produces T values for you to read is declared ? extends T; one that only consumes T values you write into it is declared ? super T; one that does both takes a plain T. Reads from ? extends T are safe because every element is at least a T; writes into ? super T are safe because a T fits any supertype container.
Common mistakes
- ✗Swapping the two — putting
? super Ton a source you only ever read from - ✗Placing a wildcard on the return type, which forces every caller to deal with the wildcard too
- ✗Reaching for a wildcard on a parameter that both reads and writes, where only an exact
Tworks
Follow-up questions
- →Why can you add a
Tto aList<? super T>but never to aList<? extends T>? - →Why does
Stream.maptake aFunction<? super T, ? extends R>rather than aFunction<T, R>?
SeniorTheoryRareWhy can't you write new T[10] in a generic class, and what do you do instead?
Why can't you write new T[10] in a generic class, and what do you do instead?
Arrays are reified while generics are erased. An array keeps its component type at runtime and checks every store against it, but new T[10] would erase to new Object[10], so it would police stores against Object and its declared type would be a lie. The compiler therefore rejects generic array creation. Work around it with (T[]) new Object[10] and an unchecked warning, with Array.newInstance and a Class<T> token, or with a List<T>.
Common mistakes
- ✗Assuming arrays are erased like generics, when they carry their component type at runtime
- ✗Believing the
(T[]) new Object[10]cast is checked, rather than an unchecked promise you must keep - ✗Letting such a
T[]escape the class, where the caller's implicit cast throwsClassCastException
Follow-up questions
- →Why does
ArrayListstore its elements in anObject[]field rather than in aT[]? - →How does a
Class<T>token letArray.newInstancebuild a genuinely typed array?
SeniorTheoryRareCan a static field or method use the class's own type parameter T?
Can a static field or method use the class's own type parameter T?
No. A class's <T> is bound per instance, while a static member belongs to the class itself — and after erasure one Box class stands behind both Box<String> and Box<Integer>, so a static context has no T to mean. static T value; and static T get() are compile errors. A static method may declare its own parameter instead, inferred per call — which is how Collections.emptyList() works.
Common mistakes
- ✗Expecting
static T value;to compile just because the class declares<T> - ✗Thinking a static method inherits the class's
<T>instead of declaring its own - ✗Assuming a
staticnested class can use the outer class's type parameter the way an inner class does
Follow-up questions
- →How does a
staticgeneric method infer its own type parameter at the call site? - →Why can an inner class use the outer class's
Twhile astaticnested class cannot?