Generics
Definition
Generics let a class or method work with a type that the caller chooses, such as List<String>. The compiler checks the types for you, so you do not need casts.
Why it exists
Before generics, collections held plain Object, so you had to cast on every read and mistakes only showed up at runtime. Generics move those mistakes to compile time.
How it works
Type erasure
The compiler checks generic types, inserts casts, then erases type arguments from the bytecode. At runtime List<String> and List<Integer> are both just List.
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true
Consequences:
| You cannot... | Because |
|---|---|
new T(), new T[10] | T does not exist at runtime |
obj instanceof List<String> | Only instanceof List<?> is legal (the argument is erased) |
Overload void f(List<String>) and void f(List<Integer>) | Same erasure: "name clash" |
Use primitives (List<int>) | Type arguments must be reference types: use List<Integer> (boxing cost) or IntStream |
| Catch or throw generic exceptions | class MyEx<T> extends Exception is illegal |
Create static fields of type T | One static field is shared by all instantiations |
Type information survives only in signatures (class files keep generic metadata), which is why frameworks can read List<Order> from a field or method via reflection, and why a type token works around erasure:
abstract class TypeRef<T> {
final Type type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
Type t = new TypeRef<List<String>>() { }.type; // java.util.List<java.lang.String>
(Jackson's TypeReference and Spring's ParameterizedTypeReference do exactly this.)
Bridge methods
When a generic class is specialised, the compiler generates synthetic bridge methods to preserve overriding after erasure:
class Box<T extends Comparable<T>> implements Comparable<Box<T>> {
T value;
public int compareTo(Box<T> other) { return value.compareTo(other.value); }
// compiler adds: public int compareTo(Object o) { return compareTo((Box) o); }
}
You see them in stack traces and when reflecting (Method.isBridge()).
Bounded type parameters
static <T extends Comparable<? super T>> T max(Collection<? extends T> items) {
Iterator<? extends T> it = items.iterator();
T best = it.next();
while (it.hasNext()) {
T next = it.next();
if (next.compareTo(best) > 0) best = next;
}
return best;
}
<T extends Comparable<? super T>> is the idiomatic bound: T must be comparable to itself or a supertype, so java.sql.Timestamp extends Date works even though only Date implements Comparable<Date>.
Multiple bounds: <T extends Number & Comparable<T>> (class first, then interfaces).
Invariance, covariance and wildcards
Arrays are covariant (and unsound); generics are invariant:
Object[] arr = new String[1];
arr[0] = 1; // compiles; ArrayStoreException at runtime
List<Object> list = new ArrayList<String>(); // does not compile: List<String> is not a List<Object>
Wildcards add variance at use sites:
| Form | Meaning | Can read as | Can write |
|---|---|---|---|
List<?> | Unknown element type | Object | only null |
List<? extends Number> | Some subtype of Number (producer) | Number | only null |
List<? super Integer> | Some supertype of Integer (consumer) | Object | Integer and subtypes |
PECS: Producer Extends, Consumer Super
static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) {
dest.add(item); // src produces T, dest consumes T
}
}
List<Number> numbers = new ArrayList<>();
copy(numbers, List.of(1, 2, 3)); // Integer -> Number
copy(numbers, List.of(1.5, 2.5)); // Double -> Number
Rule: if a parameter produces values you read, use extends; if it consumes values you write, use super; if both, use an exact type. Never use wildcards in return types (they burden callers). Comparator<? super T> in sort/max APIs and Collection<? extends E> in addAll are PECS in the JDK.
Wildcard capture
static void swapFirstTwo(List<?> list) { swapHelper(list); }
private static <T> void swapHelper(List<T> list) { // captures the unknown type as T
T tmp = list.get(0);
list.set(0, list.get(1));
list.set(1, tmp);
}
A helper method with a named type variable lets you write to a List<?>.
Reifiable types
A type is reifiable if its full type information exists at runtime: primitives, non-generic classes, raw types and unbounded wildcards (List<?>). List<String> is not reifiable. Only reifiable types can be used in instanceof, array creation and (without warnings) in varargs.
Example code
Generic methods and inference
static <K, V extends Comparable<V>> K argMax(Map<K, V> map) {
return map.entrySet().stream().max(Map.Entry.comparingByValue()).orElseThrow().getKey();
}
Java infers type arguments from arguments and the target type. Use the diamond new HashMap<>(). Where inference fails you can supply explicit arguments: Collections.<String>emptyList(). var (Java 10) only infers the local variable type; it does not change generic semantics.
System design considerations
- Public generic APIs are hard to change: keep type parameters minimal
- Prefer List<? extends Foo> parameters and return concrete List<Foo> to maximise flexibility
- Avoid leaking raw types across module boundaries
When to use it, and when not
Use it when
- You write reusable containers or algorithms that must stay type-safe
- An API accepts or returns families of types and you document variance with wildcards
- You want the compiler to remove casts
Think twice when
- You need to create instances of T or check instanceof with a parameterised type (erased at runtime)
- You build arrays of generic types
- A concrete type is clearer than a generic one
Common mistakes
Generic arrays and varargs: heap pollution
T[] array = new T[10]; // does not compile
T[] array = (T[]) new Object[10]; // unchecked cast; fine if never exposed as T[]
List<String>[] bad = new List<String>[1]; // does not compile
Heap pollution occurs when a variable of a parameterised type refers to an object that is not of that type, which surfaces later as a ClassCastException far from the cause. Generic varargs (static <T> void f(T... xs)) create a generic array and trigger "unchecked generic array creation"; mark a method @SafeVarargs only if it does not store into or expose the array.
Raw types
List list = new ArrayList(); disables checks and exists for pre-Java-5 compatibility. A raw call can silently put a String into a List<Integer> and fail later with ClassCastException. Never use raw types; if you must interface with legacy code, isolate the unchecked cast and annotate narrowly with @SuppressWarnings("unchecked").
- Using raw types or ignoring unchecked warnings.
- Wildcards in return types.
- Exposing
T[]created viaObject[]cast to callers. - Static members referring to class type parameters.
- Assuming
Class<T>andTare interchangeable at runtime.
Interview questions
1. What is type erasure and what are its consequences?
The compiler removes type arguments after checking, inserting casts as needed. You cannot instantiate type variables, create generic arrays, use instanceof with parameterised types, or overload on type arguments.
2. Explain PECS.
Use ? extends T when a collection only supplies values to you (producer), and ? super T when it only receives values (consumer). It is what makes Collections.copy and Comparator<? super T> flexible.
3. Why is List<Object> = List<String> illegal while Object[] = String[] compiles?
Generics are invariant for soundness; arrays are covariant for historical reasons and checked at runtime (ArrayStoreException).
4. What does <T extends Comparable<? super T>> mean and why super?
T can be compared using a comparator of T or any supertype, so subclasses of a comparable base class qualify.
5. How can you obtain a generic type at runtime despite erasure?
Through generic supertypes (the type-token pattern), fields/method signatures, or by passing a Class<T> argument.
6. What is a bridge method?
A compiler-generated method that preserves polymorphism after erasure, delegating from the erased signature to your typed override.
7. What is heap pollution?
A parameterised-type variable referring to an incompatible object, typically through raw types or unchecked casts, causing a delayed ClassCastException.
8. Can you create List<int>?
No. Use List<Integer> (boxing overhead and memory) or primitive-specialised structures/streams. Project Valhalla aims to lift this limitation.
Check yourself
1What does List<String> become at runtime?
2A method only reads items from a collection to sum numbers. Which parameter type follows PECS?
3Why is List<Integer> not a List<Number>?
Key takeaways
- Generics are compile-time only; erasure shapes every limitation.
- PECS decides between
extendsandsuper. - Prefer
Lists to arrays when mixing with generics. - Treat unchecked warnings as bugs unless you have proven them safe.
Finished this page? Track your progress (saved only in your browser).