Java Generics
Purpose
Get the compiler to reject casts that would otherwise fail at runtime, in a language where most
instantiated type arguments are erased from runtime object identity. Generic signatures may remain
in class-file/reflection metadata and some types are reifiable; do not equate erasure with “no
generic metadata.” Two failure modes: the codebase that opts out—
raw types, @SuppressWarnings("unchecked") on whole classes, Object parameters and casts
at the call sites — so type errors surface as ClassCastException in production; and the
signature so wildcard-heavy that callers cannot call it and nobody can read it.
Workflow
Examples target Java 21 without preview. Inspect compiler release/toolchains and resolved framework versions before changing signatures or type-token APIs; do not upgrade the project or add a serialization library to make an illustration work. References contain partial snippets unless explicitly presented as complete classes; supply imports and the enclosing declarations.
- Compile with relevant warnings on and govern them.
-Xlint:unchecked,rawtypes, and a deliberately maintained warning policy are often safer than blanket-Werroracross JDK/tool upgrades. Every unchecked warning is a place where the compiler is telling you it cannot prove what your code assumes. - Eliminate warnings from the inside out. Fix the cause (parameterise the type, use a collection instead of an array, pass a class token). Suppress only when you can prove the invariant, on the narrowest declaration possible, with a comment giving the proof.
- Parameterise types before methods. If a class holds or produces one element type, it takes a type parameter. If only one method needs one, only that method does.
- Set use-site variance from semantic data flow. A source is often
? extends T; a sink is often? super T; a parameter requiring exact read/write correlation may beT. Return types usually avoid wildcards for usability, but public families such asClass<? extends X>show legitimate exceptions. - Check the runtime boundary. Deserialisation, reflection, raw aliases or untyped caches can bypass the static contract; a typed cache/callback does not inherently lose it. Check the producer and token/validation behavior, including nested element types, before trusting values.
- Verify. No unchecked warnings; every remaining
@SuppressWarningsis one declaration wide and justified; and callers can pass the collections they already have without copying.
Rules
Avoid raw types except where required by class literals or legacy interoperation. Raw instance member types are erased under JLS rules; static members are not erased merely through a raw qualifier.
List<Object>says "any object";List<?>says "unknown element type"; a rawListbypasses element-type checks and can introduce unchecked conversions.Use
List<?>when element type is irrelevant. No non-null element can be safely added, but this is not a read-only view:clear, iterator removal, and somenullmutations remain possible. Use unmodifiable types/wrappers for immutability.Every unchecked warning is either eliminated or proven. Placing
@SuppressWarningson a class or a long method hides the next unchecked operation somebody adds there. Put it on the narrowest declaration — often a local variable extracted for that purpose — and write the one-line reason the cast is safe.Prefer lists to arrays wherever both would work. Arrays are covariant and reified (
Object[] a = new String[1]; a[0] = 1;compiles and throwsArrayStoreException); generics are invariant and erased (the same mistake does not compile). Mixing them —new List<String>[10](illegal directly) or uncheckedT[]casts—can create heap pollution when aliases allow values inconsistent with the static element type.Avoid exposing arrays whose reified runtime component type cannot honor the generic promise.
ArrayListstores anObject[]and casts elements on read; it does not make the whole backing array a truthfulT[]. Controlled unchecked array creation requires confinement and proof.Bound wildcards by direction, and usually avoid them in return types.
Collection<? extends T>for a producer,Collection<? super T>for a consumer, plainCollection<T>when the method needs exact read/write correlation. Wildcard capture can also support safe mutations such as swapping existing elements; return wildcards need the deliberate reason described in step 4.If a type parameter appears exactly once in a method signature, it should probably be a wildcard instead — and if a wildcard appears where the body needs to name the type, extract a private generic helper method to capture it.
swap(List<?>)delegating toswapHelper(List<E>)is the canonical shape.A generic/non-reifiable varargs declaration needs a heap-pollution audit;
@SafeVarargsis an assertion that the body and callees do not perform potentially unsafe operations, not a ritual requirement for all generic varargs. Avoid unsafe writes/aliases. It is legal on constructors and on static, final, or private instance methods; overridable instance methods cannot promise all implementations are safe.Use recursive bounds where the type must be comparable with itself:
<T extends Comparable<? super T>>, not<T extends Comparable<T>>— thesuperform accepts a subtype whose comparison is inherited from a base class.With an
Objectoperand,instanceof List<String>is illegal and(List<String>) valuechecks only that the object is aList, not its elements. Untyped JSON object elements may become maps while JSON strings remain strings. Pass an explicit type token (Class<T>for reifiable types,TypeReference<List<String>>,ParameterizedTypeReference) or validate the elements at the boundary.Represent "a container of many types" with a class token as key (
Map<Class<?>, Object>behind an API that casts withtype.cast(value)), not withObjectvalues that callers cast themselves. For reifiable keys,Class.castperforms a checked cast with no unchecked suppression;List.classcannot distinguish lists by their element type.Generifying an existing API is often binary compatible because erasures remain, and raw source uses may still compile with warnings, but it is not automatically compatible: erasure clashes, changed bounds/return inference, overload resolution and generated bridge methods can affect clients. Compile old source and run old binaries as compatibility tests (
java-api-design).At override boundaries, inspect erasure and compiler-generated bridge methods. Changing generic bounds or introducing an overload with the same erasure can be illegal or binary-sensitive even when parameterized source signatures look distinct.
References
- Erasure, arrays and unchecked warnings — read when a
warning cannot be eliminated obviously, when generic arrays or varargs are involved, when
ClassCastExceptionappears without a visible cast, or when deciding what a suppression must prove. - Wildcards and generic API design — read when
designing a signature callers must pass collections to, when choosing between a type
parameter and a wildcard, when a generic method needs bounds, or when inference (
var, diamond, lambdas) produces a type you did not expect. - Typesafe heterogeneous containers — read when one structure must hold values of several unrelated types — attribute maps, context propagation, plugin registries, caches keyed by type — or when a generic type must survive a serialisation boundary.