6.2 Wildcards, Type Bounds, and Type Erasure
Key Takeaways
- Wildcards (?) represent unknown types in generic references, supporting unbounded (?), upper-bounded (? extends T), and lower-bounded (? super T) variations.
- The PECS principle dictates: Producer Extends (read-only from ? extends T), Consumer Super (write-capable into ? super T).
- Lower-bounded wildcards (? super T) allow writing T and its subtypes into the collection but restrict reading to Object.
- Type erasure replaces generic type parameters with their first bound or Object at compile time, generating synthetic bridge methods to preserve polymorphic overrides.
- The @SafeVarargs annotation can only be applied to static, final, or private methods and constructors to suppress heap pollution warnings when generic varargs arrays are safe.
Wildcards, Type Bounds, and Type Erasure
Mastering generic wildcards and understanding the runtime mechanics of type erasure are essential competencies for the Oracle Certified Professional: Java SE 21 Developer (1Z0-830) exam. This section explores wildcard subtyping rules, the PECS guideline, synthetic bridge methods, heap pollution mitigation via @SafeVarargs, and the core limitations of the Java generic type system.
1. Wildcard Syntax and Categorization
While type parameters (<T>) declare formal type variables for classes, interfaces, and methods, wildcards (?) represent unknown types in variable declarations, method parameters, and field types.
Java supports three distinct forms of wildcards:
// 1. Unbounded Wildcard: Represents any type
List<?> unboundedList;
// 2. Upper-Bounded Wildcard: Represents Number or any subtype of Number
List<? extends Number> upperBoundedList;
// 3. Lower-Bounded Wildcard: Represents Integer or any supertype of Integer
List<? super Integer> lowerBoundedList;
Syntax Restriction: Bounds on Type Parameters vs. Wildcards
- Type Parameter Declarations: Can use
extendsonly (e.g.,<T extends Number>). Lower bounds on type parameters (<T super Integer>) are illegal syntax and will not compile! - Wildcards: Can use either
extends(? extends Number) orsuper(? super Integer).
2. The PECS Principle: Producer Extends, Consumer Super
The PECS rule, coined by Joshua Bloch, provides an infallible guideline for choosing wildcard bounds:
- Producer Extends (
? extends T): If a parameterized collection produces data (you read elements from it), use? extends T. - Consumer Super (
? super T): If a parameterized collection consumes data (you write elements into it), use? super T. - Exact Type (
T): If you both read from and write to the collection, do NOT use wildcards.
Read/Write Capabilities Matrix
| Wildcard Type | Example | Read Capability | Write Capability (Mutation) |
|---|---|---|---|
List<?> | List<?> | Reads return Object | Cannot add any element except literal null |
List<? extends T> | List<? extends Number> | Reads return T (Number) | Cannot add any element except literal null |
List<? super T> | List<? super Integer> | Reads return Object | Can add instances of T and subtypes of T |
Detailed Walkthrough: Collections.copy
The signature of Collections.copy perfectly illustrates the PECS principle in the Java standard library:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
// Read from src (Producer: ? extends T) -> returns T
T item = src.get(i);
// Write to dest (Consumer: ? super T) -> accepts T
dest.set(i, item);
}
}
List<Number> destList = new ArrayList<>(List.of(0, 0, 0));
List<Integer> srcList = List.of(10, 20, 30);
// Compiles and executes perfectly:
// src is a Producer of Integers (? extends Number)
// dest is a Consumer of Numbers (? super Integer)
Collections.copy(destList, srcList);
System.out.println(destList); // [10, 20, 30]
Why You Cannot Add to List<? extends Number>
Consider the following attempt:
List<? extends Number> numbers = new ArrayList<Integer>();
// numbers.add(Integer.valueOf(10)); // COMPILE ERROR!
The compiler knows numbers refers to a list of some specific subtype of Number, but it cannot determine whether that concrete type is List<Integer>, List<Double>, List<BigDecimal>, or List<Byte>. Allowing numbers.add(Integer.valueOf(10)) would cause heap corruption if the underlying reference were actually ArrayList<Double>(). Therefore, the compiler completely disallows adding any object to ? extends T collections (except null, which belongs to all reference types).
3. Subtyping and the Wildcard Hierarchy
Wildcards establish polymorphic relationships between generic types that would otherwise be invariant:
Collection<?>
▲
│
List<?>
▲ ▲
│ │
List<? extends Number> List<? super Integer>
▲ ▲
│ │
List<Number> List<Integer>
▲
│
List<Integer> (is a subtype of List<? extends Number>, but NOT of List<Number>)
List<Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // VALID: Integer extends Number
List<? super Integer> superList = new ArrayList<Number>(); // VALID: Number is super of Integer
List<?> wildList = numList; // VALID: List<?> is root of all List types
4. Type Erasure Mechanics and Bridge Methods
Java implemented generics using type erasure to ensure binary compatibility with pre-existing pre-Java 5 libraries and JVM bytecode.
What javac Does During Erasure:
- Replaces all formal type parameters with their first bound (or
Objectif unbounded). - Inserts synthetic type casts where necessary to maintain type safety.
- Generates bridge methods in subclasses to preserve polymorphism during method overriding.
Type Erasure in Action
// Source Code
public class Node<T extends Number> {
private T data;
public Node(T data) { this.data = data; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
}
// Bytecode Representation after Type Erasure
public class Node {
private Number data; // T erased to first bound: Number
public Node(Number data) { this.data = data; }
public Number getData() { return data; }
public void setData(Number data) { this.data = data; }
}
Synthetic Bridge Methods
When a class extends a parameterized class or implements a parameterized interface with a concrete type argument, type erasure can create mismatched method signatures:
public class IntegerNode extends Node<Integer> {
public IntegerNode(Integer data) { super(data); }
// Overridden method with concrete parameter
@Override
public void setData(Integer data) {
System.out.println("IntegerNode: " + data);
super.setData(data);
}
}
After erasure, the parent class has setData(Number data), while IntegerNode has setData(Integer data). Because their parameter types differ, IntegerNode.setData(Integer) would overload rather than override Node.setData(Number).
To resolve this, javac automatically generates a synthetic bridge method in IntegerNode:
// Synthetic Bridge Method generated by javac in IntegerNode.class:
public void setData(Number data) {
this.setData((Integer) data); // Delegates to the specific method with downcast
}
5. Heap Pollution and the @SafeVarargs Annotation
What is Heap Pollution?
Heap pollution occurs when a variable of a parameterized type references an object that is not of that parameterized type, usually caused by raw types or unchecked generic varargs arrays:
public class HeapPollutionDemo {
public static void pollute(List<String>... stringLists) {
Object[] objArray = stringLists; // Varargs creates an array: List<String>[]
objArray[0] = List.of(42); // Heap pollution! Integer list inside List<String>[]
String value = stringLists[0].get(0); // Throws ClassCastException at runtime!
}
}
The @SafeVarargs Annotation
Because varargs (T...) implicitly creates an array (T[]), combining generics and varargs produces a compiler warning (Possible heap pollution from parameterized vararg type).
The @SafeVarargs annotation asserts to the compiler that the method implementation is safe from heap pollution:
@SafeVarargs
public static <T> List<T> asUnmodifiableList(T... elements) {
// SAFE: elements array is only read, never modified or leaked
return List.of(elements);
}
Mandatory Rules for @SafeVarargs:
- Can ONLY be applied to methods and constructors that take a varargs parameter of a non-reifiable type.
- Can ONLY be applied to:
staticmethodsfinalinstance methodsprivateinstance methods (added in Java 9)- Constructors
- It CANNOT be applied to non-final, non-private instance methods because overriding subclasses could introduce heap pollution.
- The method implementation must not store anything into the varargs array and must not allow a reference to the varargs array to escape.
6. Fundamental Generic Restrictions and Limitations
The 1Z0-830 exam frequently tests edge cases where generics cannot be used:
- Cannot Instantiate Type Parameters Directly:
new T()is illegal becauseTis erased toObjector its bound at runtime. - Cannot Create Generic Arrays Directly:
new T[10]andnew List<String>[10]are illegal compile errors. Unbounded generic arrays likenew List<?>[10]are permitted. - Cannot Use Primitive Types as Type Arguments:
List<int>is illegal; you must use wrapper types likeList<Integer>. - Cannot Use
instanceofwith Parameterized Types:obj instanceof List<String>fails compilation because type arguments are erased at runtime. Testingobj instanceof List<?>is legal. - Cannot Create, Catch, or Throw Generic Exception Classes:
class GenericException<T> extends Exceptionis illegal.try { ... } catch (T e)is illegal.- Exception: Using a type parameter in a
throwsclause (public <T extends Throwable> void execute() throws T) is legal!
- Cannot Overload Methods with Identical Erased Signatures:
public class AmbiguousOverload { // COMPILE ERROR: Both methods erase to 'void process(List)' public void process(List<String> list) { } public void process(List<Integer> list) { } }
Given the following variable declarations, which of the listed operations will compile successfully without errors?
Examine the following class containing overloaded method declarations: public class DataProcessor { public void handle(List<String> items) { } public void handle(List<Integer> items) { } } What is the compilation result for DataProcessor?
Which of the following generic declarations or operations is legal and compiles without error in Java?
You need to design a utility method that reads elements from a source list of numbers and appends them to a target list. Following the PECS principle, what is the most flexible and correct method signature?