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.
Last updated: September 2026

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 extends only (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) or super (? 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 TypeExampleRead CapabilityWrite Capability (Mutation)
List<?>List<?>Reads return ObjectCannot 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 ObjectCan 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:

  1. Replaces all formal type parameters with their first bound (or Object if unbounded).
  2. Inserts synthetic type casts where necessary to maintain type safety.
  3. 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:

  1. Can ONLY be applied to methods and constructors that take a varargs parameter of a non-reifiable type.
  2. Can ONLY be applied to:
    • static methods
    • final instance methods
    • private instance methods (added in Java 9)
    • Constructors
  3. It CANNOT be applied to non-final, non-private instance methods because overriding subclasses could introduce heap pollution.
  4. 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:

  1. Cannot Instantiate Type Parameters Directly: new T() is illegal because T is erased to Object or its bound at runtime.
  2. Cannot Create Generic Arrays Directly: new T[10] and new List<String>[10] are illegal compile errors. Unbounded generic arrays like new List<?>[10] are permitted.
  3. Cannot Use Primitive Types as Type Arguments: List<int> is illegal; you must use wrapper types like List<Integer>.
  4. Cannot Use instanceof with Parameterized Types: obj instanceof List<String> fails compilation because type arguments are erased at runtime. Testing obj instanceof List<?> is legal.
  5. Cannot Create, Catch, or Throw Generic Exception Classes:
    • class GenericException<T> extends Exception is illegal.
    • try { ... } catch (T e) is illegal.
    • Exception: Using a type parameter in a throws clause (public <T extends Throwable> void execute() throws T) is legal!
  6. 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) { }
    }
    
Loading diagram...
PECS Data Flow and Type Erasure with Bridge Methods
Test Your Knowledge

Given the following variable declarations, which of the listed operations will compile successfully without errors?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Which of the following generic declarations or operations is legal and compiles without error in Java?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D