Wildcards, Bounded Generics & The PECS Rule
Master advanced generic variance. Understand invariance, covariance with <? extends T>, contravariance with <? super T>, and Joshua Bloch's PECS rule.
By the end, you’ll be able to…
- Explain why List<Integer> is NOT a subtype of List<Number> (Type Invariance).
- Apply covariance (<? extends T>) for read-only producer collections.
- Apply contravariance (<? super T>) for write-only consumer collections using the PECS principle.
01The Invariance Trap
In Java, arrays are covariant: Integer[] is a subtype of Number[]. While this seems intuitive, array covariance is notoriously type-unsafe: you can assign an Integer[] to a Number[] variable and store a Double into it, triggering a runtime ArrayStoreException.
To prevent this catastrophic flaw, Java Generics are Invariant. List<Integer> is distinctly NOT a subtype of List<Number>. Even though Integer extends Number, a List<Integer> cannot be passed to a method expecting List<Number>. If this were allowed, the method could insert a Double into your integer list!
02The PECS Principle: Producer Extends, Consumer Super
How do we write flexible, reusable generic methods that accept subtypes or supertypes safely? Joshua Bloch formulated the golden rule of Java generics in Effective Java: PECS (Producer Extends, Consumer Super).
1. Producer Extends (<? extends T>): Use when the collection produces data (you only read from it). You can read items as type T, but you CANNOT add anything to the collection (except null) because the exact concrete subtype is unknown. 2. Consumer Super (<? super T>): Use when the collection consumes data (you only write to it). You can safely write T and its subclasses into the collection, but reading items only yields Object.
03Unbounded Wildcards and Bounded Type Parameters
An Unbounded Wildcard (List<?>) represents a list of unknown type. It is useful when method logic only relies on capabilities provided by Object or List methods that do not depend on type parameters (such as list.size() or list.clear()).
Multiple Bounds (<T extends Comparable<T> & Serializable>) allow constraining generic parameters across both classes and interfaces, enabling powerful algorithmic guarantees at compile time.
04Why This Matters in Production
The standard Java Collections Framework is the quintessential real-world demonstration of PECS. Inspect Collections.copy(List<? super T> dest, List<? extends T> src): destination consumes data (super), and source produces data (extends).
Failing to use wildcards in library API signatures restricts callers unnecessarily, forcing consumers to copy collections or cast types. Applying PECS makes public APIs intuitive, flexible, and completely type-safe.
05Production Architecture & Engineering Trade-Offs
In large-scale enterprise codebases, rigorous domain modeling relies on enforcing structural invariants at the type level rather than relying on defensive runtime validation. Modern Java leverages sealed types and records to turn runtime type errors into compile-time exhaustiveness guarantees. When designing domain transfer objects or persistent entities, ensuring immutability protects against unintended side effects across concurrent worker threads.
Furthermore, avoiding deep inheritance hierarchies in favor of composition and interface contracts eliminates fragile base class anti-patterns. By encapsulating state mutations within well-defined service boundaries and exposing pure functional transformers, software architects build maintainable, audit-compliant systems that scale smoothly across distributed cloud infrastructure.
The ideas worth keeping.
- Java Generics are invariant by default to protect collection integrity.
- Use <? extends T> when you only read from a collection (Producer Extends).
- Use <? super T> when you only write to a collection (Consumer Super).
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Exercise 1: Implementing Collections.copy with PECS
Write a generic method copy(List<? super T> dest, List<? extends T> src) that copies elements from src to dest safely.
Explore a worked solution
public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T item : src) { dest.add(item); }}src is the producer (extends T), while dest is the consumer (super T), fulfilling the PECS rule.
Exercise 2: Maximum Element with Bounded Generics
Write a generic method findMax(List<? extends T> list) for any type T that extends Comparable<? super T>.
Explore a worked solution
public static <T extends Comparable<? super T>> T findMax(List<? extends T> list) { if (list.isEmpty()) throw new IllegalArgumentException("Empty list"); T max = list.get(0); for (T item : list) { if (item.compareTo(max) > 0) max = item; } return max;}Comparable<? super T> ensures comparison works even if T inherits compareTo from a superclass.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Exercise 1: Implementing Collections.copy with PECS
Write a generic method copy(List<? super T> dest, List<? extends T> src) that copies elements from src to dest safely.
Build successful. (Java 15) [Process exited normally]
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.