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.
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:
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:
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.
Comments
Read or comment on GitHub (sign-in required to comment)