Generics
Java generics are erased: the runtime does not see the actual type arguments, and List<String> and List<Integer> are the same runtime class. The mechanism is a substantial source of confusion for programmers coming from C# (where generics are reified and the type arguments are visible at runtime) and from C++ (where templates produce separate code per instantiation). Java generics are nonetheless adequate for the principal use cases — type-safe collections, the streams API, generic algorithms — and the bounded wildcards admit substantial flexibility in API design.
This page covers generic types, generic methods, type parameters and bounds, the wildcard mechanism, the consequences of type erasure, and the conventions for using each idiomatically. The relationship to the standard library’s collections and streams is in Data structures and Streams.
Generic types
A generic type takes one or more type parameters in its declaration:
public class Box<T> {
private final T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> bs = new Box<>("hello"); // diamond operator (since Java 7)
Box<Integer> bi = new Box<>(42);
The diamond <> (since Java 7) admits omitting the type arguments on the right-hand side; the compiler infers them from the left-hand side.
The type parameter T stands in for an actual type; the compiler verifies type-safety at the call site. At runtime, however, Box<String> and Box<Integer> are the same class — the type parameter has been erased.
Multiple type parameters use a comma-separated list:
public class Pair<F, S> {
private final F first;
private final S second;
public Pair(F first, S second) {
this.first = first;
this.second = second;
}
public F first() { return first; }
public S second() { return second; }
}
The conventional naming:
Tfor a single type parameter.T1,T2, … orT,U,Vfor multiple parameters with no semantic distinction.K,Vfor key-value pairs (inMap<K, V>).Efor collection element types (inList<E>).Rfor return types (inFunction<T, R>).
Generic methods
A method may have its own type parameters, declared before the return type:
public static <T> T first(List<T> list) {
if (list.isEmpty()) throw new NoSuchElementException();
return list.get(0);
}
String s = first(List.of("a", "b", "c")); // T = String
Integer n = first(List.of(1, 2, 3)); // T = Integer
The compiler infers T from the argument type. The explicit form Map.<String>first(...) is rare; the inference works for nearly every case.
Generic methods may appear in non-generic classes:
public class Algorithms {
public static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T x : list) {
if (x.compareTo(best) > 0) best = x;
}
return best;
}
}
The <T extends Comparable<T>> is a bounded type parameter: T must be a type that implements Comparable<T>. The bound enables the body to call compareTo on T values.
Bounded type parameters
Type parameters may be constrained:
<T extends Number> // T must extend Number (a class)
<T extends Comparable<T>> // T must implement Comparable<T>
<T extends Number & Comparable<T>> // T must extend Number AND implement Comparable<T>
Bounds may name a class, an interface, or a combination (using &). The bound determines what methods may be called on a T value; without a bound, T is treated as Object.
The extends keyword is used regardless of whether the bound is a class or an interface — Java’s generic syntax does not distinguish.
Wildcards: ?, ? extends T, ? super T
A wildcard admits using a generic type without specifying the actual type argument:
List<?> unknown; // a list of some unknown type
List<? extends Number> nums; // a list of Number or some subtype
List<? super Integer> ints; // a list of Integer or some supertype
The three forms are:
- Unbounded (
?) — any type. Read-only. - Upper-bounded (
? extends T) —Tor any subtype. Read-only asT. - Lower-bounded (
? super T) —Tor any supertype. Write-only asT.
Wildcards are the principal mechanism for designing generic APIs that accept a family of types rather than a single type:
public static double sum(Collection<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.0, 2.0, 3.0);
sum(ints); // OK: List<Integer> is a Collection<? extends Number>
sum(doubles); // OK: List<Double> is a Collection<? extends Number>
Without the wildcard, sum(Collection<Number>) would not accept a List<Integer> because Java generics are invariant: List<Integer> is not a subtype of List<Number>.
The PECS rule
The Joshua-Bloch-coined mnemonic for wildcards: Producer Extends, Consumer Super.
- If the parameter produces
Tvalues that you read out of it, use? extends T. - If the parameter consumes
Tvalues that you write into it, use? super T.
public static <T> void copy(List<? super T> dst, List<? extends T> src) {
for (T item : src) {
dst.add(item);
}
}
List<Number> numbers = new ArrayList<>();
List<Integer> ints = List.of(1, 2, 3);
copy(numbers, ints); // dst is `super Number` (Number itself); src is `extends Number` (Integer)
The rule is the conventional answer for “should this be extends, super, or neither?”.
Generic constructors
A constructor may have its own type parameters:
public class Container<T> {
private final List<T> items;
public <U extends T> Container(Collection<U> initial) {
this.items = new ArrayList<>(initial);
}
}
The <U extends T> introduces a constructor-specific type parameter, distinct from the class’s T. The construction is rare; the conventional alternative is a generic factory method.
Type erasure
At runtime, the type parameter is erased — the JVM sees List rather than List<String>. The principal consequences:
List<String> and List<Integer> are the same class
List<String> s = new ArrayList<>();
List<Integer> i = new ArrayList<>();
s.getClass() == i.getClass(); // true: both are ArrayList
The runtime cannot distinguish the two; reflection sees only the raw ArrayList.
Cannot create generic arrays
T[] array = new T[10]; // ERROR: cannot create generic array
The runtime cannot reify T[]. The conventional substitute is (T[]) new Object[10] (with an unchecked-cast warning) or Array.newInstance(...):
@SuppressWarnings("unchecked")
T[] array = (T[]) new Object[10];
// Or, with Class<T>:
T[] array = (T[]) Array.newInstance(elementClass, 10);
Cannot test against a parameterized type
if (obj instanceof List<String>) { /* ERROR */ }
if (obj instanceof List<?>) { /* OK */ }
The runtime cannot test the type argument; only the raw type is testable.
Cannot use T.class or new T()
public <T> T create() {
return new T(); // ERROR: cannot instantiate type parameter
}
The conventional substitute is to take a Class<T> argument:
public <T> T create(Class<T> cls) throws Exception {
return cls.getDeclaredConstructor().newInstance();
}
The Class<T> reifies the type at the call site; the method receives the runtime Class and uses reflection.
Bridge methods
The compiler synthesises bridge methods to preserve polymorphism across generic and non-generic boundaries. The mechanism is invisible to user code but occasionally surfaces in stack traces and reflection:
public class IntList implements Comparable<IntList> {
@Override
public int compareTo(IntList other) { /* ... */ }
}
// The compiler synthesises:
// public int compareTo(Object other) { return compareTo((IntList) other); }
// to satisfy the raw Comparable interface.
The bridge method is necessary because the raw Comparable interface (which the JVM sees) declares compareTo(Object).
Heap pollution and @SafeVarargs
A generic method that takes a varargs of a generic type is the source of heap pollution: the array’s runtime type is the erased element type, but the compile-time type promises the element-type-with-arguments:
@SafeVarargs
public static <T> List<T> asList(T... items) {
return Arrays.asList(items);
}
The @SafeVarargs annotation suppresses the unchecked-warning at the declaration site; the conventional discipline is to apply it only when the method’s body genuinely does not write to the varargs array (which would be the actual hazard).
Reified vs erased generics
Java’s erased generics differ substantially from C#‘s reified generics:
| Aspect | Java (erased) | C# (reified) |
|---|---|---|
| Runtime type-argument visibility | No | Yes |
| Reflection sees the type arguments | No | Yes |
typeof(T) admissible | No | Yes |
| Generic specialisation per type | No | Yes |
| Boxing of value types in generics | Yes (autoboxing) | No |
new T() | Requires Class<T> | Requires new() constraint |
The trade-offs:
- Erased generics preserve full backward compatibility (legacy non-generic code interoperates with generic code through raw types) and produce a single class per generic type (smaller binaries, simpler runtime).
- Reified generics admit better runtime type-checking, no boxing of value types, and more powerful reflection.
The decisions were driven by Java 5’s compatibility requirements; reified generics would have broken every collection class in the existing standard library. Project Valhalla’s eventual specialised generics may add reified generics for value types in a backward-compatible way.
Idiomatic use
The conventional patterns:
Constrain at the API boundary
public <T extends Comparable<T>> T max(List<T> list) { /* ... */ }
The bound is at the public interface; the body uses T freely.
PECS for collection parameters
public static <T> void copy(List<? super T> dst, List<? extends T> src) { /* ... */ }
public static double sum(Collection<? extends Number> numbers) { /* ... */ }
Producer-extends, consumer-super; the wildcards make the API maximally flexible.
Factory methods over generic constructors
public static <T> Box<T> of(T value) {
return new Box<>(value);
}
Box<String> b = Box.of("hello"); // type inferred from the argument
Factory methods admit better type inference than generic constructors.
Use the diamond operator
Map<String, List<Integer>> map = new HashMap<>();
The right-hand side does not need to repeat the type arguments.
Avoid raw types
List rawList = new ArrayList(); // raw type; compiler warning
List<String> typedList = new ArrayList<>(); // OK
Raw types are admitted for backward compatibility but produce unchecked-warnings; modern code uses parameterised types throughout.
A note on what Java generics do not have
Features that Java generics lack, with their workarounds:
| Feature | Java | Workaround |
|---|---|---|
| Reified type arguments | No | Pass Class<T> |
| Specialisation for primitives | No | Use primitive specialisations: IntStream instead of Stream<int> |
| Higher-kinded types | No | Encode through library tricks (rare in practice) |
| Variance on classes | No (only via wildcards at use sites) | The wildcard mechanism approximates |
| Default type arguments | No | Provide a non-generic façade that fills in the default |
The combination of the bounded type-parameter mechanism, the wildcard system, and the PECS rule covers the substantial majority of practical generic-programming use cases. The cases where Java’s generics are inadequate are typically also cases where the additional library complexity would not be justified by the increased expressiveness.