A generic declaration uses type parameters to express compile-time relationships among inputs, outputs, and element types without copying an implementation for every concrete type.
List<Integer> is not a subtype of List<Number>. A wildcard doesn’t disable checking either: ? extends T and ? super T permit different read and write operations.
Declare <T> when an implementation must preserve one type, use ? extends T for an API parameter that only produces values, use ? super T for one that only consumes them, and investigate every unchecked warning.
What it is and why it exists
Java generics let you declare parameterized types and generic methods. In List<String>, String is a type argument that fixes the list’s element type. In class Box<T>, T is a type parameter that a use site supplies with a concrete type.
The main benefit isn’t merely avoiding a cast; generics preserve relationships among types. static <T> T first(List<T> values) says that the result and list elements have the same T. Changing the parameter to List<?> and the result to Object still lets the method read an element, but discards the relationship its caller needs.
The compiler checks those relationships at each use site. Adding an integer to List<String>, treating Optional<Integer> as Optional<Number>, or supplying a type argument that violates a bound all fail during compilation. Generics can’t prove that business data is valid, but they rule out a class of container-content and API-wiring errors.
You meet generics throughout collections, Optional, Stream, Comparator, asynchronous APIs, and framework extension points. In your own API, they fit a case where the algorithm doesn’t care about the concrete type but several positions must agree, or where input can come from a family of types. If an implementation only makes sense for one type, adding <T> hides the real constraint.
Java only permits reference types as generic type arguments, so you write List<Integer>, not List<int>. Autoboxing makes most calls look natural, but nullability, allocation, and numeric equality still follow the wrapper type’s contract.
How it works
A type parameter appears after a declaration name or before a method’s return type. A class’s T can occur in instance fields and instance method parameters and results. A static member belongs to no particular instance, so it can’t directly use the class’s T; a static method may declare its own <T>, which is independent even when the names match.
The compiler usually infers type arguments from arguments, target types, and bounds, so new ArrayList<String>() can often become new ArrayList<>(). You can still put an explicit type argument before a method name, as in Warehouse.<Number>first(values). Do that only when inference needs help or the spelling clarifies intent.
Generic types are invariant. Although Integer is a subtype of Number, List<Integer> isn’t a subtype of List<Number>; otherwise a method receiving the latter could add a Double and violate the original list’s element contract. Arrays are covariant and check stores at runtime, so don’t transfer their rules to generics.
A wildcard represents a constrained but unknown type argument. It suits an API use site, not an implementation that repeatedly needs to name a type relationship. The compiler conservatively grants these read and write capabilities:
| Form | Safe read type | Safe values to write | Typical role |
|---|---|---|---|
List<T> | T | T | One known type |
List<?> | Object | Only null | Any list, observation only |
List<? extends T> | T | Only null | Producer of T |
List<? super T> | Object | T and its subtypes | Consumer of T |
PECS abbreviates “Producer Extends, Consumer Super.” Production and consumption describe what a parameter does relative to a method, not a container class’s permanent identity. When a parameter both yields and accepts the same T, you usually need a named type parameter instead of forcing either wildcard.
An upper bound restricts substitutions and exposes the bound’s members: <T extends Number> lets an implementation call doubleValue(). In an intersection bound, a class bound must come first when present, followed by interfaces, as in <T extends Number & Comparable<T>>. The recursive form <T extends Comparable<? super T>> lets T inherit a natural order declared for a supertype and is often more flexible than Comparable<T>.
Type parameters and wildcards solve different problems. <T> names an unknown type so several positions can retain a relationship; ? says the caller doesn’t need to know that type’s name. If a type occurs once, an extra <T> often adds no relationship. If the same unknown type must connect parameters or a result, a wildcard may be too vague.
Deriving capabilities from a signature
You don’t need to reconstruct the compiler’s entire inference process to read a complex generic signature. Start with one call site, rewrite every type variable and wildcard as a constraint, then check whether those constraints support the method body’s operations.
- Find the type parameters introduced by the declaration, such as
<T>at the start of a method. - Mark every parameter, result, and bound position where the same
Toccurs. - Promise only reads of the upper bound for each
? extendsparameter and writes of the lower bound for each? superparameter. - Check that the call-site types satisfy all constraints together instead of judging each argument independently.
- Finally, check whether the result type preserves a relationship that callers can use.
For example, static <T> T choose(T left, T right) doesn’t require the arguments to have identical runtime classes. It asks the compiler to infer one T that can contain both. Assigning the result to a broad target can still compile, but the caller may voluntarily discard more precise information.
Declaration sites and use sites
A Java class or interface declares its type parameters and upper bounds at its declaration site, as in class Box<T extends Item>. A wildcard occurs where a parameterized type is used, as in List<? extends Item>; it doesn’t change the declaration of List, but restricts the operations that are safe through this reference.
| Design need | Suitable signature shape | Reason |
|---|---|---|
| Input and output have one type | <T> T convert(T value) | Names and preserves the relationship |
| Traverse any list | void inspect(List<?> values) | Does not depend on element type |
| Read from a subtype collection | void read(List<? extends T> values) | Preserves a safe upper bound |
| Write into a supertype collection | void write(List<? super T> values) | Preserves a safe lower bound |
| Call a member on each element | <T extends Bound> | Exposes the bound’s API inside the implementation |
A public signature should offer the flexibility callers actually need, rather than putting a wildcard at every position. An overly narrow List<T> rejects safe calls, while an overly broad List<?> erases useful result relationships. The smallest complete set of constraints is usually easiest to use and test.
Examples
These four programs build from type relationships through PECS, bounds and wildcard capture, then show erasure’s runtime view. Every output was produced locally with OpenJDK 21.0.12 by compiling with javac --release 21 -Xlint:all and running the class; the syntax and APIs remain valid in the target Java 25.
Declaring and inferring type parameters
Bin<T> lets each instance choose its payload type independently of the label. first() infers T for each call. Its result retains the list’s element type, so the caller needs no cast.
import java.util.List;
public class Warehouse {
record Bin<T>(String label, T item) {}
static <T> T first(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("items must not be empty");
}
return items.getFirst();
}
public static void main(String[] args) {
var book = new Bin<>("A-12", "Effective Java");
var counts = List.of(3, 5, 8);
System.out.println(book.label() + ": " + book.item());
System.out.println(first(counts).getClass().getSimpleName()
+ ": " + first(counts));
}
}A-12: Effective Java
Integer: 3The diamond <> lets the constructor infer Bin<String> from its target, while var only removes repeated spelling from a local declaration; it doesn’t make the variable dynamically typed. The initializer still determines counts’ static type.
An empty list has no element to return, so the method rejects it explicitly. Using null as a sentinel would add an implicit null protocol to an otherwise clear result relationship. If absence has domain meaning, a caller can choose a more explicit interface such as Optional<T>.
Connecting different lists with PECS
The source only supplies T to the method, so it’s a producer; the destination only receives T, so it’s a consumer. One signature can append a List<Integer> to a List<Number> while preserving compile-time checking.
import java.util.ArrayList;
import java.util.List;
public class CopyOrders {
static <T> void appendAll(
List<? super T> destination,
List<? extends T> source) {
destination.addAll(source);
}
static double total(List<? extends Number> values) {
double result = 0.0;
for (Number value : values) {
result += value.doubleValue();
}
return result;
}
public static void main(String[] args) {
List<Integer> dailyOrders = List.of(3, 5, 2);
List<Number> report = new ArrayList<>();
appendAll(report, dailyOrders);
System.out.println(report);
System.out.println(total(report));
}
}[3, 5, 2]
10.0Inside appendAll(), a value from source is at least a T, while destination promises that it can receive a T. Reading from destination gives only Object, because its actual element type might be T, a supertype of T, or Object.
total() only reads numbers, so it needn’t restrict callers to exactly List<Number>. It can’t add an Integer or Double to values, because the actual list may hold some other Number subtype.
Combining bounds and wildcard capture
The bound on max() guarantees comparable elements. swapFirstTwo() hands its unknown type to a private helper that names it. This wildcard capture lets the implementation safely take out and put back the same unknown element type.
import java.util.ArrayList;
import java.util.List;
public class BoundsAndCapture {
static <T extends Comparable<? super T>> T max(
List<? extends T> values) {
if (values.isEmpty()) {
throw new IllegalArgumentException("values must not be empty");
}
T result = values.getFirst();
for (T value : values) {
if (value.compareTo(result) > 0) {
result = value;
}
}
return result;
}
static void swapFirstTwo(List<?> values) {
swap(values, 0, 1);
}
private static <T> void swap(List<T> values, int left, int right) {
T saved = values.get(left);
values.set(left, values.get(right));
values.set(right, saved);
}
public static void main(String[] args) {
var queues = new ArrayList<>(List.of("fast", "bulk", "slow"));
swapFirstTwo(queues);
System.out.println(queues);
System.out.println(max(queues));
}
}[bulk, fast, slow]
slowThe public method doesn’t expose the helper type parameter to callers. When it invokes swap(), the compiler creates a capture type for the unknown element of List<?> and keeps it consistent within that call.
The bound only guarantees that compareTo() is available; it doesn’t prove the order is appropriate for the business domain. The API contract must still define its ordering rule, empty-list policy, and whether null is allowed. Generics can’t replace those domain decisions.
Observing erased runtime information
ArrayList instances with different type arguments share one runtime class, yet a field declaration can retain a generic signature for reflection. These facts don’t conflict: an object usually doesn’t know its list element type, while a declaration site in a class file can carry signature metadata.
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class ErasureView {
private List<String> names = List.of("Ada");
public static void main(String[] args) throws Exception {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
Object candidate = strings;
System.out.println("same runtime class: "
+ (strings.getClass() == integers.getClass()));
Field field = ErasureView.class.getDeclaredField("names");
System.out.println("field class: " + field.getType());
System.out.println("field signature: " + field.getGenericType());
System.out.println("list<?> check: " + (candidate instanceof List<?>));
}
}same runtime class: true
field class: interface java.util.List
field signature: java.util.List<java.lang.String>
list<?> check: truefield.getType() returns the erased List class, while getGenericType() reads the declaration signature. The type argument of the local variable strings doesn’t become a runtime tag on each list object, so you can’t write candidate instanceof List<String>.
List<?> is reifiable and can appear in instanceof, because the check only needs to establish that the object is some kind of List. A successful check still leaves the element type unknown; it doesn’t turn the value into List<String>.
Pitfalls
Lifting a subtype relation into a generic container
Fix: choose List<? extends Number> or List<? super Integer> from the parameter’s data flow. When an implementation truly reads and writes one element type, declare a named type parameter and use it in every related position.
Escaping a compiler error with a raw type
Fix: run javac -Xlint:all to find raw types and unchecked conversions, then prove the source of each value. Keep suppression scopes as small as possible and document the invariant that makes the conversion safe instead of writing “compiler false positive.”
Treating extends as a writable upper bound
Fix: use List<? super Integer> when the method writes integers. If it both reads and writes the same element type, name that type with <T>; don’t probe a wildcard’s actual type by attempting non-null writes.
Assuming type arguments are always available at runtime
Fix: when runtime type information is part of the contract, pass a Class<T>, a controlled type token, or a parser explicitly and define what kinds of types it represents. Don’t mistake reflection metadata on a field signature for information present on every object.
Applying array rules to generics
Fix: prefer List<T> in most code. When an array API is unavoidable, ask the caller for an array factory or Class<T>, contain any unavoidable cast in one verifiable boundary, and test the wrong-component-type path.
Erasure and runtime boundaries
The Java compiler erases a type parameter to its leftmost bound, or usually to Object when it has no explicit bound. It inserts casts where needed and may generate bridge methods to preserve polymorphic behavior after an override. Erasure describes compilation output; it doesn’t mean the compiler ignores generics while checking source.
The instantiations of a parameterized type normally share one class instead of generating separate classes for Box<String> and Box<Integer>. Static fields likewise belong to the generic class itself and can’t hold one value per type argument. This differs from mechanisms such as C++ templates that generate code per instantiation.
Reifiable types
A reifiable type has a sufficiently complete runtime representation for operations that need runtime checking. Primitive types, non-generic classes, raw types, parameterized types whose arguments are all unbounded wildcards, and some array types are reifiable. List<String> and the type parameter T aren’t.
That distinction explains why candidate instanceof List<?> is legal and candidate instanceof List<String> isn’t. The first checks only raw list identity; the second would also require a runtime check of the erased element argument. Pattern matching doesn’t recover a missing type argument.
new T() and T.class are unavailable for the same reason: T doesn’t automatically supply one concrete class object at runtime. If construction is part of the API, accept a Supplier<? extends T>. If you must operate on a runtime class, accept Class<T>, while remembering that it can’t fully represent a nested parameterized type such as List<String>.
Raw types, unchecked warnings, and heap pollution
Raw types exist mainly for compatibility with code written before generics. List isn’t the same as List<Object>: the former bypasses parts of generic checking and causes unchecked warnings, while the latter explicitly accepts only operations allowed by its Object relationship. Treat a raw type as a migration boundary, not shorthand in new code.
Heap pollution occurs when a variable of a parameterized type refers to an object that doesn’t satisfy its declared type. Assignments through raw types, unchecked casts, and some generic-varargs operations can create this state. The failure often appears later as ClassCastException at a compiler-inserted cast.
Generic varargs use an array whose parameterized element type may not be reifiable, so a declaration or call can produce a warning. @SafeVarargs is the programmer’s safety promise about the implementation; it neither verifies nor repairs dangerous code. Use it only after confirming that the method doesn’t store incompatible values, expose the array, or invoke it unsafely.
Wildcard capture
The compiler introduces a fresh internal type for each wildcard expression, a process called wildcard capture. Consequently, reading from List<?> and immediately writing back to it can still fail to type-check in some direct expressions. A private generic helper can name the capture for one operation.
A capture keeps its identity only within the corresponding expression or invocation. Two independent List<?> parameters aren’t guaranteed to have the same element type even when both happen to contain strings at runtime. To move elements safely between lists, connect them in the public signature with <T>, ? extends T, and ? super T.
Erasure, overriding, and overloading
When a subclass specializes and overrides a generic superclass method, erasure can leave different bytecode signatures. The compiler may generate a synthetic bridge method that forwards the erased call to the specialized override. Reflection tools and stack traces can therefore expose a bridge member that isn’t explicitly present in source.
Overloads must remain distinguishable after erasure. process(List<String>) and process(List<Integer>) both erase to a method taking List, so they can’t be declared together. Use different method names or raw parameter types, or unify the behavior in one generic implementation; neither result types nor type arguments distinguish overloads.
Type inference and API evolution
Type inference solves constraints at a source call site; it isn’t a runtime check. Argument types, target types, invocation context, and declared bounds can all contribute. The same generic call may infer differently in different target positions. When an error is opaque, splitting a nested expression into locals with explicit static types usually reveals the conflict better than adding a cast.
Target typing and capture diagnostics
The diamond, generic methods, and lambdas can all use a target type. Moving an expression from an assignment into a context with no target may remove constraints that previously made it inferable. That changes the information available to the compiler, not runtime behavior.
Names such as CAP#1 in a diagnostic denote capture types the compiler created for wildcards; they aren’t classes you should declare in source. When a message says that two positions can’t be proven to have the same type, return to the signature and find the missing relationship. If the relationship exists, a helper generic method can name it. If it doesn’t, a cast can’t manufacture a proof.
Changing a published signature
A generic signature serves source checking and affects what callers can compile. Widening a parameter from List<T> to List<? extends T> may admit more source calls, but it also removes writes that the implementation can perform. Changing a bound may make previously legal type arguments fail when callers recompile.
An unchanged erased descriptor doesn’t guarantee a harmless change. Bridge methods, overload selection, compiler-inserted result casts, and generic signatures read through reflection may all be affected. For a published library, test old binary callers, newly compiled source callers, and reflection consumers separately instead of checking only the current module.
A repair order for failed inference
Establish the relationship the API intends before changing its syntax. Use this order to investigate:
- Give intermediate expressions meaningful local static types to locate the conflicting constraints.
- Check that
extendsandsuperfollow the direction of data flow. - Decide whether several parameters truly share one
Tor need separate type parameters. - Use explicit type arguments or a local cast only when the contract can prove them safe.
Turning every variable into a raw type usually removes the error along with its most useful evidence. Keep warnings visible and shrink the failing expression so you can distinguish an overly narrow API, an invalid caller type, and a compiler that needs more target information.
See Java Type Erasure for the full erasure rules, signature attributes, and bridge details, and Java Wildcards and PECS for further wildcard API derivations. This topic keeps only the overlap needed to design a generic interface.
Further reading
4 questions · 1 predict-the-output · 1 spot-the-bug