6.2 Bounded Type Parameters and Wildcards (PECS Rule)
Key Takeaways
Generic types in Java are invariant: even if Sub extends Super,
Generic<Sub>is not a subtype ofGeneric<Super>, protecting collections against heap pollution.Bounded type parameters (
<T extends Super>) restrict allowable types at declaration; multiple bounds mandate that a class bound must appear first before any interface bounds (<T extends Number & Comparable<T>>).Upper bounded wildcards (
<? extends T>) denote covariant subtyping for read-only producer collections; no elements other than null can be inserted into them.Lower bounded wildcards (
<? super T>) denote contravariant subtyping for consumer collections into which elements of type T (and its subclasses) can be safely added.The PECS guideline (Producer Extends, Consumer Super) determines wildcard selection, while private generic helper methods resolve wildcard capture compiler errors.
6.2 Bounded Type Parameters and Wildcards (PECS Rule)
A foundational characteristic of Java generics is invariance. In Java, while an Integer is a subtype of Number, a List<Integer> is not a subtype of List<Number>. This strict invariance prevents invalid elements from being stored in parameterized collections. However, invariance can make method signatures overly rigid. To enable flexible, type-safe APIs, Java provides bounded type parameters and wildcards (?). Mastering the interactions between upper bounds, lower bounds, and the PECS rule is essential for the 1Z0-829 examination.
The Invariance Principle vs. Array Covariance
Java arrays are covariant: Integer[] is considered a subtype of Number[]. This legacy behavior permits dangerous assignments that the compiler allows but that fail at runtime:
Integer[] intArr = new Integer[5];
Number[] numArr = intArr; // Allowed due to array covariance
// Compiles cleanly, but throws ArrayStoreException at runtime!
numArr[0] = Double.valueOf(3.14);
To eliminate runtime array store errors, Java generics enforce strict invariance:
List<Integer> intList = new ArrayList<>();
// COMPILE ERROR: incompatible types: List<Integer> cannot be converted to List<Number>
// List<Number> numList = intList;
If the assignment were legal, executing numList.add(Double.valueOf(3.14)) would contaminate intList with a Double, destroying type safety. Invariance prevents this at compile time.
Bounded Type Parameters
Bounded type parameters restrict the concrete types that can be passed as type arguments to a generic class, interface, or method.
Upper Bounds on Type Parameters
An upper bound specifies that a type variable must be a subtype of a specific class or interface using the extends keyword:
public <T extends Number> double computeDoubleValue(T number) {
// Legal: Number methods are guaranteed to be available on T
return number.doubleValue();
}
Multiple Bounds
A type parameter can specify multiple bounds using the ampersand (&) operator. The Java Language Specification enforces strict ordering rules for multiple bounds:
- If one of the bounds is a class, it must appear first in the bounds list.
- A type parameter can extend at most one class (due to single class inheritance).
- Any number of interface bounds may follow the class bound.
// VALID: Class bound (Number) is first, followed by interface bound (Comparable)
public class NumericHolder<T extends Number & Comparable<T>> {
private T value;
public boolean isGreaterThan(T other) {
return value.compareTo(other) > 0;
}
}
// COMPILE ERROR: interface bound cannot precede class bound
// public class InvalidHolder<T extends Comparable<T> & Number> {}
// COMPILE ERROR: cannot extend multiple classes
// public class DualClass<T extends Number & String> {}
Warning
No Lower Bounds on Type Parameters!
You can never use the super keyword to define lower bounds on type parameter declarations. Syntax such as <T super Integer> or class Box<T super Number> is completely illegal and will not compile. The super keyword is permitted exclusively on wildcards (<? super T>).
Wildcards and Variance
A wildcard, denoted by the question mark ?, represents an unknown type in parameterized code. Wildcards provide variance, allowing collections of different parameterized types to be referenced polymorphically.
1. Unbounded Wildcards (<?>)
An unbounded wildcard represents an unknown type. List<?> is the supertype of all generic List instantiations (List<String>, List<Integer>, etc.).
- Read Operations: Safe. Elements can be read and assigned to
Objectreferences. - Write Operations: Prohibited. You cannot add any object to a
List<?>except the literalnull.
List<?> wildcardList = new ArrayList<String>(List.of("A", "B"));
Object item = wildcardList.get(0); // Valid: returns Object
// wildcardList.add("C"); // COMPILE ERROR: cannot add String to List<?>
wildcardList.add(null); // Valid: null is compatible with any reference type
2. Upper Bounded Wildcards (<? extends T>)
An upper bounded wildcard restricts the unknown type to a specific type T or any subtype of T. It introduces covariant subtyping:
List<? extends Number> numbers = new ArrayList<Integer>(List.of(1, 2, 3));
// Reading is permitted as Number
Number n = numbers.get(0); // Valid: guaranteed to be at least Number
double d = n.doubleValue();
// Writing is STRICTLY FORBIDDEN (except null)
// numbers.add(Integer.valueOf(10)); // COMPILE ERROR!
// numbers.add(Double.valueOf(2.5)); // COMPILE ERROR!
numbers.add(null); // Valid
Why does numbers.add(Integer.valueOf(10)) fail to compile? Because the underlying reference could be an ArrayList<Double>. Allowing an Integer to be added would violate type safety.
3. Lower Bounded Wildcards (<? super T>)
A lower bounded wildcard restricts the unknown type to a specific type T or any supertype of T (up through Object). It introduces contravariant subtyping:
List<? super Integer> consumers = new ArrayList<Number>();
// Writing is SAFE for instances of Integer or subtypes of Integer
consumers.add(Integer.valueOf(42)); // Valid: Number and Object lists can hold Integers
consumers.add(100); // Valid: autoboxing produces Integer
// consumers.add(Double.valueOf(3.14)); // COMPILE ERROR: Double is not an Integer
// Reading is restricted to Object
Object obj = consumers.get(0); // Valid: only guaranteed to be Object
// Integer num = consumers.get(0); // COMPILE ERROR: cannot assign Object to Integer without cast
The PECS Principle (Producer Extends, Consumer Super)
Coined by Joshua Bloch in Effective Java, the PECS rule provides a clear mnemonic for selecting wildcard bounds in method parameter signatures:
- Producer Extends (
? extends T): If your method reads data from a collection (the collection produces items for your code to use), declare the parameter with<? extends T>. - Consumer Super (
? super T): If your method writes data into a collection (the collection consumes items supplied by your code), declare the parameter with<? super T>. - Both Read and Write: If your method must both read from and write into the collection, do not use wildcards at all; declare an exact type parameter
<T>.
Canonical PECS Example: Collections.copy
The standard library method Collections.copy demonstrates PECS in action:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i)); // Reads T from src (producer), writes T into dest (consumer)
}
}
This signature permits copying a List<Integer> (src produces integers) into a List<Number> (dest consumes numbers), maximizing API flexibility without sacrificing type safety.
Wildcard Capture and Helper Methods
When the compiler encounters a wildcard, it internally captures a specific but anonymous type variable (e.g., capture#1 of ?). If code attempts to write a captured wildcard value back into the collection, the compiler flags a type mismatch:
public void swapFirstTwo(List<?> list) {
// COMPILE ERROR: set(int, capture#1-of ?) cannot be applied to (int, capture#2-of ?)
// list.set(0, list.get(1));
}
Even though reading list.get(1) produces an element from the same list, the compiler treats the return type of get and the accepted parameter of set as distinct captured types.
To resolve this wildcard capture error, introduce a private generic helper method that names the captured type parameter:
public void swapFirstTwo(List<?> list) {
swapHelper(list); // Wildcard is captured as concrete type T
}
private <T> void swapHelper(List<T> list) {
T temp = list.get(1);
list.set(0, temp); // Compiles cleanly: temp has exact type T accepted by set
}
Summary of Wildcard Capabilities
| Wildcard Syntax | Type Category | Read Return Type | Allowed Insertions | Typical Usage |
|---|---|---|---|---|
List<?> | Unbounded | Object | null only | Read-only inspection, operations independent of type (size(), clear()) |
List<? extends T> | Upper Bounded (Covariant) | T | null only | Producer: Reading data out of the collection |
List<? super T> | Lower Bounded (Contravariant) | Object | T and subtypes of T, null | Consumer: Writing data into the collection |
List<T> | Exact Invariant | T | T and subtypes of T, null | Both reading and writing data |
Given the following variable declarations:
List<? extends Number> producers = new ArrayList<Integer>(List.of(1, 2, 3));
List<? super Integer> consumers = new ArrayList<Number>();
Which of the following lines compiles without any compiler errors?
producers.add(10);
consumers.add(10);
Integer val = consumers.get(0);
producers.add(Double.valueOf(2.5));
Which of the following generic type parameter declarations is syntactically valid in Java?
public class PriorityQueue<T extends Number & Comparable<T>>
public class PriorityQueue<T super Integer>
public class PriorityQueue<T extends Number & Integer>
public class PriorityQueue<T extends Comparable<T> & Number>
Given the class hierarchy where Integer extends Number and Number extends Object, which of the following variable assignments fails to compile?
List<?> list4 = new ArrayList<String>();
List<Number> list2 = new ArrayList<Integer>();
List<? extends Number> list1 = new ArrayList<Integer>();
List<? super Integer> list3 = new ArrayList<Number>();
Sections you finish are checked off in the contents.