Skip to main content
Java · Topic 7 of 18

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 exceptionsclass MyEx<T> extends Exception is illegal
Create static fields of type TOne 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:

FormMeaningCan read asCan write
List<?>Unknown element typeObjectonly null
List<? extends Number>Some subtype of Number (producer)Numberonly null
List<? super Integer>Some supertype of Integer (consumer)ObjectInteger 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 via Object[] cast to callers.
  • Static members referring to class type parameters.
  • Assuming Class<T> and T are 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​

Quick quiz3 questions

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 extends and super.
  • Prefer Lists to arrays when mixing with generics.
  • Treat unchecked warnings as bugs unless you have proven them safe.
Next topicException handling

Finished this page? Track your progress (saved only in your browser).