Generics (Java 5) let classes, interfaces, methods and constructors take types as
parameters, so that one List class gives type-checked lists of strings,
numbers or anything else. The checks are made at compile time; at run time most type
arguments are erased (JLS 4.4 to 4.9, 8.1.2, 8.4.4 and
chapter 18).
Type parameters
TypeParameters:
< TypeParameter {, TypeParameter} >
TypeParameter:
{TypeParameterModifier} TypeIdentifier [TypeBound]
TypeBound:
extends TypeVariable
extends ClassOrInterfaceType {& InterfaceType}
class Box<T> { // generic class
private T value;
Box(T value) { this.value = value; }
T get() { return value; }
<R> Box<R> map(Function<? super T, ? extends R> f) { // generic method
return new Box<>(f.apply(value));
}
}
interface Pair<K, V> { K key(); V value(); } // generic interface
class Sorted<T extends Comparable<? super T>> { } // bounded, with a recursive bound
static <T extends Number & Comparable<T>> T min(T a, T b) { // two bounds
return a.compareTo(b) > 0 ? b : a;
}
- A bound restricts the type arguments and lets the body use the bound’s members.
Without a bound, it is
Object. - With several bounds, the first may be a class or interface and the rest must be
interfaces, joined by
&. If the first bound is a type variable, there can be no others. - A class’s type parameters cannot be used in its static members or static initialisers.
- Type arguments must be reference types:
List<int>is an error; useList<Integer>.
Type arguments, the diamond and explicit arguments
| Form | Example | Since |
|---|---|---|
| Full type arguments | new HashMap<String, List<Integer>>() | 5 |
| Diamond: inferred from the context | Map<String, List<Integer>> m = new HashMap<>(); | 7 |
| Diamond with an anonymous class | new Comparator<>() { … } | 9 |
| Explicit type arguments to a generic method | Collections.<String>emptyList(), this.<T>helper() | 5 |
| Explicit type arguments to a generic constructor | new <String>Wrapper(x) | 5 |
Explicit type arguments to a method need something before the dot, such as a class
name or this; <String>emptyList() alone is not valid.
Subtyping and invariance
Parameterised types are invariant: List<String> is not a
subtype of List<Object>, even though String is a subtype
of Object. If it were, code could add an Integer to a list of
strings through the List<Object> view. The subtypes of a parameterised
type come from its class hierarchy with the same arguments
(ArrayList<String> is a List<String>) and from
wildcards.
Wildcards
| Wildcard | Means | Can read as | Can write |
|---|---|---|---|
List<?> | A list of some unknown type | Object | only null |
List<? extends Number> | A list of Number or some subtype | Number | only null |
List<? super Integer> | A list of Integer or some supertype | Object | Integer and its subtypes |
The usual guide is “producer extends, consumer super”: a
parameter the method reads from takes ? extends T, one it writes to takes
? super T:
static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) dest.add(item);
}
List<Number> numbers = new ArrayList<>();
copy(numbers, List.of(1, 2, 3)); // T is Integer: reads Integers, writes into a List<Number>
Each time an expression of a wildcard type is used, capture conversion
gives the wildcard a fresh, unnamed type variable (CAP#1 in compiler
messages). Two uses of the same List<?> are not known to have the same
element type, which is why a helper method with a named type parameter is needed to
swap two elements of a List<?>:
static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); }
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i))); // E captures the wildcard
}
Type erasure
Generic types are compiled to code that uses the erasure of each type
(JLS 4.6): type arguments are dropped, and a type variable becomes the erasure of its
leftmost bound. List<String> erases to List, and
T extends Comparable<T> to Comparable. The compiler adds
casts where needed, and bridge methods where an override’s erased signature
differs from the method it overrides.
Because of erasure, at run time a List<String> and a
List<Integer> are the same class. This gives these rules:
| Not allowed | Why, and what to do |
|---|---|
new T(), new T[n] | T is not known at run time; pass a Supplier<T> or a Class<T> |
new List<String>[10] | Generic array creation; use List<?>[] or a list of lists |
x instanceof List<String> | Allowed since Java 16 only when the compiler can prove it safe (for example when x is a Collection<String>); otherwise use List<?> |
List<String>.class | There is one class object: List.class |
catch (T e), a generic subclass of Throwable | The handler could not be checked at run time |
Overloads f(List<String>) and f(List<Integer>) | Both erase to f(List): a name clash |
A static field of type T | Static members are shared by every parameterisation |
| Primitive type arguments | Erasure needs reference types; boxing is used instead |
A type whose values carry all their type information at run time is
reifiable (JLS 4.7): primitive types, non-generic types, raw types,
parameterisations with only unbounded wildcards (List<?>), and arrays
of these.
Raw types
A generic class name used without type arguments, List, is a
raw type (JLS 4.8). Raw types exist so code written before Java 5 still
compiles. Their members are seen with erased types, and using them gives unchecked
warnings. New code should not use them, except in class literals
(List.class) and instanceof tests where
List<?> would do as well.
Heap pollution and varargs
Heap pollution is a variable of a parameterised type referring to an object
that is not of that type, which can only happen through unchecked operations. A
generic varargs parameter, T... items, creates an array of a
non-reifiable type and so is a possible source; the compiler warns at the declaration
and at calls. @SafeVarargs on the method promises that it does not store
into the array or let it escape, and removes the warnings:
@SafeVarargs
static <T> List<T> listOf(T... items) {
return new ArrayList<>(Arrays.asList(items));
}
Type inference
The compiler infers type arguments for generic method calls, the diamond, lambda
parameters and var (JLS chapter 18). It reduces the call to constraints
on inference variables, from the arguments and, since Java 8, from the target
type, the type the context expects:
List<String> a = Collections.emptyList(); // T inferred as String from the target
List<String> b = new ArrayList<>(); // diamond
Comparator<String> c = Comparator.comparing(String::length); // T is String, U is Integer
var d = List.of(1, 2.5); // List<Number & Comparable<…> & …>
An expression whose type depends on its target, such as a generic method call, a diamond, a lambda or a conditional, is a poly expression. Inference can fail, for example when a lambda has no target from which to learn its parameter types; then explicit type arguments or a cast help.
Intersection types
An intersection type A & B is a value of both types. It appears as a
bound, as an inferred type, and, since Java 8, in a cast, mainly to give a lambda more
than one interface:
Runnable r = (Runnable & Serializable) () -> System.out.println("saved");