Generics - Wildcards & Type Erasure
Java generics use type erasure: the compiler performs type checking, but at runtime the type parameters are erased to their bounds (or Object if unbounded). Wildcards—?, ? extends T, and ? super T—express “read-only / write-only” or “some subtype / supertype” constraints, making it safer to read and write collections. This article explains erasure, wildcard semantics, and PECS with examples and tables.
Overview
- Type erasure: Generic information is removed after compilation.
List<String>at runtime is justList. You cannotnew T(), useT.class, or haveList<String>.class(onlyList.class). Bridge methods preserve polymorphism. - ? extends T (upper bound): “Some subtype of T”. Safe to read as T; cannot write (except null). Use for producers or read-only parameters.
- ? super T (lower bound): “Some supertype of T”. Safe to write T and its subtypes; can only read as Object. Use for consumers or write-only parameters.
- PECS: Producer extends, Consumer super. Producers use extends; consumers use super.
Example
Example 1: Erasure — no generics at runtime
JavaList<String> list = new ArrayList<>(); // After compilation: List list = new ArrayList(); // list.add("a") becomes list.add((Object)"a"); elements are cast to String on get
- You cannot write
if (obj instanceof List<String>)ornew T(). For runtime types, passClass<T>or explicit type parameters.
Example 2: extends — read-only
Javavoid print(List<? extends Number> list) { for (Number n : list) { System.out.println(n); } // list.add(1); // Compile error: ? might be Integer or Double, cannot write } print(Arrays.asList(1, 2)); print(Arrays.asList(1.0, 2.0));
- Accepts
List<Integer>,List<Double>, etc., and reads safely as Number; you cannot add (except null).
Example 3: super — write-only
Javavoid addNumbers(List<? super Integer> list) { list.add(1); list.add(2); // Integer x = list.get(0); // Compile error: get returns Object } List<Number> nums = new ArrayList<>(); addNumbers(nums);
- You can write Integer into
List<Number>,List<Object>, etc.; reads return Object only.
Example 4: PECS in Collections.copy
Java// From java.util.Collections public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T t : src) dest.add(t); }
srcis a producer (you read from it) →extends T.destis a consumer (you write to it) →super T.
Example 5: Comparison and PECS
| Wildcard | Can read as | Can write | Typical use |
|---|---|---|---|
| ? extends T | T and subtypes | No (only null) | Producer, read-only parameter |
| ? super T | Object only | T and subtypes | Consumer, write-only parameter |
| ? | Object | Only null | Rarely; when type is fully unknown |
- PECS: If the parameter produces elements (you read from it) → use
extends. If it consumes elements (you write to it) → usesuper.
Example 6: Raw types and unchecked casts
JavaList raw = new ArrayList(); // Raw type, no generics raw.add("hello"); List<String> strList = raw; // Unchecked; may fail at runtime
- Avoid raw types; use generics or wildcards so the compiler catches errors.
Core Mechanism / Behavior
- Erasure: Generic types are replaced by their bounds at compile time (e.g.
<T extends Comparable>→ Comparable); unbounded → Object. Casts and checks happen at compile time. - Bridge methods: When a subclass overrides a generic method, the compiler generates synthetic methods so polymorphism works with erased signatures.
- Wildcards: The compiler allows only calls that are safe for the given bound; it cares about “what can be read” and “what can be written,” not the exact type of
?. - Reification: Java generics are not reified; you cannot inspect type parameters at runtime. Use
Class<T>or type tokens when you need runtime type information.
Type Tokens and Runtime Types
When you need the concrete type at runtime (e.g. for deserialization), pass Class<T>:
Javapublic <T> T fromJson(String json, Class<T> type) { return objectMapper.readValue(json, type); } User u = fromJson(str, User.class);
- Super Type Token: For
List<String>, useTypeReference(Jackson) or a similar pattern, since erasure removesList<String>at runtime.
Key Rules
- For “read-only / producer” use ? extends T; for “write-only / consumer” use ? super T; PECS helps choose.
- When you need the concrete type at runtime, pass Class<T> or a type token; do not rely on T after erasure.
- Avoid raw types and unchecked casts; express constraints with generics or wildcards so the compiler finds bugs.
- Prefer bounded type parameters (
<T extends Comparable<T>>) when you need to call methods on T inside the generic code.
What's Next
See Reflection & Annotations for Class<T> and generics. See Collections for generics and wildcards in the standard library.