Java Generics

Last update: 2025-02-02 05:23:41 +0800 +0800
tags: Generics | Java
Available translations: 日本語 / 中文(简体) / 中文(繁體)

Java generics provide compile-time type checking while preserving compatibility with pre-generics bytecode. The compiler erases type arguments, inserts casts where required, and may generate bridge methods to preserve overriding after erasure. Consequently List<String> and List<Integer> have the same runtime class, and generic type arguments cannot reliably be recovered from an object.

Invariance and Bounds #

Generic classes are invariant: List<Integer> is not a List<Number>, because the latter would permit inserting a Double. Wildcards express variance at an API boundary.

java
static double sum(List<? extends Number> values) {
    return values.stream().mapToDouble(Number::doubleValue).sum();
}

static void addDefaults(List<? super Integer> destination) {
    destination.add(0);
}

Use extends for a producer from which callers read and super for a consumer into which callers write. A List<? extends Number> can produce Number values but accepts no non-null value; a List<? super Integer> accepts Integer values but produces only Object. “Read-only” and “write-only” are shortcuts, not capability rules: either list can still be queried, cleared, or receive null where the implementation permits it.

Capture and Inference #

Each wildcard denotes an unknown captured type. A wildcard container cannot express that two independently wildcarded values have the same type, so use a generic helper when the relationship matters:

java
static void reverse(List<?> list) { reverseCaptured(list); }
private static <T> void reverseCaptured(List<T> list) {
    Collections.reverse(list);
}

Method inference combines argument constraints, target type, and declared bounds. The diamond operator infers type arguments for constructor calls; it does not make a raw type safe. Bounds can require capabilities: <T extends Comparable<? super T>> T max(List<? extends T> values). An intersection bound may include one class followed by interfaces.

Construction Under Erasure #

new T(), new T[n], and T.class are illegal because erasure does not retain the constructor, component type, or class literal needed at runtime. Pass a factory instead:

java
static <T> T create(Supplier<? extends T> factory) {
    return factory.get();
}

For reflective integration, accept Class<T> and handle its checked reflective failures, but prefer Supplier<T> when callers own construction. Generic arrays remain restricted because arrays are reified and covariant while generic types are erased and invariant.

References #

Comments

Read or comment on GitHub (sign-in required to comment)