What Is PECS in Java? Producer Extends, Consumer Super
PECS in Java stands for Producer Extends, Consumer Super — the rule for choosing the bound of a wildcard in a generic method parameter. If a parameter is a producer (a data source the method only reads T values from), declare it as <? extends T>. If it is a consumer (a destination the method only writes T values into), declare it as <? super T>. If the method both reads and writes, use the exact type T with no wildcard. The PECS principle was named by Joshua Bloch in “Effective Java” (3rd edition, Item 31).
Why does it matter? Write a method static double sum(List<Number> list), pass it a List<Integer>, and the compiler refuses: incompatible types: List<Integer> cannot be converted to List<Number>. Integer is a subclass of Number, yet a list of integers is not a list of numbers. The fix is one change in the signature — List<? extends Number> — and PECS tells you when to write extends and when super.
The PECS rule: extends and super in one table
The PECS rule says: a collection the method takes elements from is declared with ? extends T, and a collection the method puts elements into is declared with ? super T. The wildcard bound decides two things at once: which collections can be passed to the method and which operations are allowed on them inside it.
The rule at a glance for the hierarchy Person ← Employee ← Manager (used in every example of this lesson):
| Parameter declaration | Lists you can pass | What get() returns | What you can add() | Role |
|---|---|---|---|---|
List<Employee> | Only List<Employee> | Employee | Employee, Manager, null | Reads and writes |
List<? extends Employee> | List<Employee>, List<Manager> | Employee | Only null | Producer (source) |
List<? super Employee> | List<Employee>, List<Person>, List<Object> | Object | Employee, Manager, null | Consumer (destination) |
List<?> | Any list | Object | Only null | Read as Object |
In one line: get values out — extends, put values in — super, do both — no wildcard.
How to tell a producer from a consumer
Look at what the method does with the parameter, not at the collection type. Naftalin and Wadler’s book “Java Generics and Collections” calls the same idea the Get and Put Principle. It is not limited to collections: a Comparator<T> receives objects in its compare() method, so it consumes them — that is why the JDK declares it as Comparator<? super T>.
Three kinds of wildcards and the example hierarchy
A wildcard is the question mark inside angle brackets, meaning “some unknown type”. Java generics have three forms: the unbounded wildcard <?>, the upper bounded wildcard <? extends T> and the lower bounded wildcard <? super T>. PECS answers the question of which of the two bounded forms to choose.
List<?> anyList = ...; // unbounded wildcard
List<? extends Employee> employees = ...; // upper bounded wildcard
List<? super Employee> staff = ...; // lower bounded wildcard All examples use a simple hierarchy: Employee extends Person, and Manager extends Employee.

class Person {}
class Employee extends Person {}
class Manager extends Employee {} <? extends Employee>— upper bounded wildcard:Employeeis the upper bound, soEmployeeand all its subclasses (Manager) fit.Personis above the bound and does not.<? super Employee>— lower bounded wildcard:Employeeis the lower bound, soEmployeeand all its superclasses (Person,Object) fit.Manageris below the bound and does not.
Why collections are generic at all and what goes wrong with raw types is shown in Generic collections; the general theory of type parameters and type erasure is in the lesson on Java generics.
Producer: <? extends T> — read only
A parameter declared as <? extends T> is a producer: the method can read its elements as T but cannot add anything except null. In return, the method accepts a collection of T and a collection of any subclass of T.
public static void processUpperBounded(List<? extends Employee> employees) {
employees.add(new Employee()); // compile-time error
employees.add(null); // compiles
Employee employee = employees.get(0); // reading as Employee is fine
} The line with add(new Employee()) fails with incompatible types: Employee cannot be converted to CAP#1. CAP#1 is the compiler’s name for “some unknown subtype of Employee” (wildcard capture): the argument could be a List<Manager>, and a plain Employee must not get into it. null fits any reference type, so it can be added.
Which collections can be passed to this method:
List<Person> persons = new ArrayList<>();
List<Employee> employees = new ArrayList<>();
List<Manager> managers = new ArrayList<>();
processUpperBounded(persons); // compile-time error: Person is above the bound
processUpperBounded(employees); // OK
processUpperBounded(managers); // OK A practical example is the average of a list of any numbers. Without a wildcard you would need separate methods for List<Integer> and List<Double>. The method only reads the elements, so the list is a producer and List<? extends Number> is enough:
import java.util.List;
public class AverageDemo {
public static double getAverage(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum / list.size();
}
public static void main(String[] args) {
List<Integer> iList = List.of(1, 5, 8, 9);
List<Double> dList = List.of(1.5, 2.5, 3.5, 4.5);
System.out.println(getAverage(iList)); // Output: 5.75
System.out.println(getAverage(dList)); // Output: 3.0
System.out.println(getAverage(List.of())); // Output: NaN
}
} An empty list throws no exception: in double arithmetic 0.0 / 0 gives NaN. If that result is not acceptable, check list.isEmpty() first.
Consumer: <? super T> — write only
A parameter declared as <? super T> is a consumer: the method can write objects of type T and its subclasses into it, but can read an element back only as Object. Such a method accepts a collection of T and a collection of any superclass of T.
public static void processLowerBounded(List<? super Employee> employees) {
employees.add(new Person()); // compile-time error
employees.add(new Employee()); // OK
employees.add(new Manager()); // OK
Object first = employees.get(0); // reading is possible only as Object
if (first instanceof Employee e) { // type check and cast (Java 16+)
System.out.println("It is an Employee: " + e);
}
Employee employee = employees.get(0); // compile-time error
} Which collections can be passed to this method:
processLowerBounded(persons); // OK
processLowerBounded(employees); // OK
processLowerBounded(managers); // compile-time error: Manager is below the bound The last line of the method fails with incompatible types: CAP#1 cannot be converted to Employee, because the argument could be a List<Object>: the only thing known for sure is that an element is an Object. Reading from a consumer is technically possible but useless without an instanceof check — which is why such a parameter is called a destination, not a source.
Why you can pass List<Person> but cannot add a Person
The bound ? super Employee both allows passing a List<Person> and forbids add(new Person()). This is not an exception to PECS but two consequences of one rule: the compiler allows only the operations that are safe for every list that could stand behind the parameter.
At runtime, a List<? super Employee> may be a List<Employee>, a List<Person> or a List<Object>. Let’s check what is safe for all three:
add(new Manager())andadd(new Employee())— the object is anEmployee, aPersonand anObjectat the same time, so it fits any of the three lists.add(new Person())— would fit aList<Person>, but if aList<Employee>was actually passed, it would receive an object that is not anEmployee. The compiler does not know which list was passed, so it forbids the operation.
The same logic applies to variable assignment: the bound limits which collection can be assigned, not only which one can be passed to a method.
List<? extends Employee> list1 = employees; // OK
List<? extends Employee> list2 = managers; // OK
List<? super Employee> list3 = persons; // OK
List<? super Employee> list4 = managers; // compile-time error PECS in your own method
When a method moves data from one collection to another, PECS is applied to each parameter separately: the source gets extends, the destination gets super. The method then accepts the widest range of type combinations and stays type-safe. Here is a complete Java PECS example:
import java.util.ArrayList;
import java.util.List;
public class HireDemo {
static class Person {}
static class Employee extends Person {}
static class Manager extends Employee {}
// candidates is a producer, staff is a consumer
static void hireAll(List<? extends Employee> candidates, List<? super Employee> staff) {
for (Employee e : candidates) {
staff.add(e);
}
}
public static void main(String[] args) {
List<Manager> managers = List.of(new Manager(), new Manager());
List<Person> persons = new ArrayList<>();
hireAll(managers, persons);
System.out.println(persons.size()); // Output: 2
}
} If both parameters were declared as List<Employee>, the call hireAll(managers, persons) would not compile because of either argument.
PECS in the Java standard library
The PECS rule is visible in JDK signatures: methods of the Collections class and the collection interfaces that read from one parameter and write to another are declared exactly by this rule. If you forget where extends goes and where super goes, look at these signatures.
| Method | Signature | Producer and consumer |
|---|---|---|
Collections.copy | <T> void copy(List<? super T> dest, List<? extends T> src) | src is the producer, dest is the consumer |
Collections.addAll | <T> boolean addAll(Collection<? super T> c, T... elements) | c is the consumer |
Collection.addAll | boolean addAll(Collection<? extends E> c) | The argument is the producer |
List.sort | void sort(Comparator<? super E> c) | The comparator consumes the elements |
Collections.max | <T> T max(Collection<? extends T> coll, Comparator<? super T> comp) | The collection is the producer, the comparator is the consumer |
What does this code print?
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class PecsJdkDemo {
public static void main(String[] args) {
// copy: src = List<Integer> (producer), dest = List<Number> (consumer)
List<Integer> ints = List.of(3, 1, 2);
List<Number> numbers = new ArrayList<>(List.of(0, 0, 0, 0.5));
Collections.copy(numbers, ints);
System.out.println(numbers);
// addAll: strings go into a List<Object>
List<Object> objects = new ArrayList<>();
Collections.addAll(objects, "a", "b");
System.out.println(objects);
// sort: a Comparator<Object> works for a List<Integer>
Comparator<Object> byString = Comparator.comparing(Object::toString);
List<Integer> list = new ArrayList<>(List.of(10, 9, 100));
list.sort(byString);
System.out.println(list);
// copy into an empty list
Collections.copy(new ArrayList<Integer>(), ints);
}
} [3, 1, 2, 0.5]
[a, b]
[10, 100, 9]
Exception in thread "main" java.lang.IndexOutOfBoundsException: Source does not fit in dest Explanation: Collections.copy overwrites the first elements of dest and leaves the tail (0.5) alone. The list of Integer values is sorted as strings, so "100" comes before "9" — one comparator for Object works with any list thanks to ? super E. The last call fails: copy does not grow the destination list, so the size of dest must be at least the size of src. To copy into an empty list, use dest.addAll(src) or new ArrayList<>(src).
The other methods of the Collections class are covered in Collections Class in Java, and building comparators is explained in Comparable and Comparator.
get(0) or getFirst()
The examples use list.get(0). The getFirst() method was added to List only in Java 21 (the SequencedCollection interface), so code with it does not compile on Java 17. The PECS rules are the same for it: getFirst() on a List<? extends Employee> returns Employee, and on a List<? super Employee> it returns Object.
Common mistakes with PECS
Most PECS mistakes come from putting a wildcard out of habit instead of by the parameter’s role, or from expecting more from it than it guarantees. These are the cases that show up most often in code reviews and interviews.
| Mistake | What happens | How to fix it |
|---|---|---|
Expecting List<Manager> to be a subtype of List<Employee> | Generic types are invariant: the call does not compile | Declare the source parameter as List<? extends Employee> |
Adding to a List<? extends T> | incompatible types: ... cannot be converted to CAP#1 | If the method writes to the collection, use ? super T or the exact T |
Treating ? extends as protection from changes | remove(), clear() and add(null) still work | For immutability use List.copyOf() or Collections.unmodifiableList() (see Immutable collections) |
Reading from a List<? super T> as T | Compile-time error: the element type is Object | If you need to both read as T and write, use the exact type List<T> |
A wildcard in the return type: List<? extends Number> load() | The caller cannot add anything to the result and has to deal with the wildcard itself | Wildcards belong in parameters only; return List<Number> or List<T> |
Summary
The PECS rule comes down to one question about each method parameter: does the method read from it or write to it? The answer determines the wildcard bound.
- Producer (a source parameter, the method only reads) —
<? extends T>: accepts collections ofTand its subclasses, reads asT, writes onlynull. - Consumer (a destination parameter, the method only writes) —
<? super T>: accepts collections ofTand its superclasses, writesTand its subclasses, reads only asObject. - The method both reads and writes — the exact type
T, no wildcard. - Wildcards are used in method parameters, not in return types.
- One bound limits both which collection can be passed or assigned and which operations are allowed inside — these are not two separate rules.
Frequently Asked Questions
What is the difference between List<?> and List<Object>?
A List<?> parameter accepts a list of any type, such as List<String> or List<Integer>, but you can add only null to it. A List<Object> accepts any element, but you cannot pass a List<String> to it — that is a different type. List<?> suits methods that do not care about the element type: size(), clear(), printing.
How is PECS related to covariance and contravariance?
? extends T makes a type covariant (a List<Manager> fits where a List<? extends Employee> is expected), and ? super T makes it contravariant. Java arrays are always covariant and pay for it at runtime: Object[] arr = new String[1]; arr[0] = 1; throws ArrayStoreException. Generic types are invariant by default, and PECS gives you flexibility without such exceptions.
Can a wildcard have both extends and super?
No: a wildcard has only one bound, so <? extends A super B> does not compile, and neither does <? extends A & B>. Multiple upper bounds are allowed only for a type parameter: <T extends Number & Comparable<T>>. Conversely, super is not allowed for a type parameter — <T super Integer> does not compile.
Why does Collections.copy() fail with an empty destination list?
PECS is about types, not size. Collections.copy(dest, src) replaces elements of dest by index and never adds new ones, so when dest.size() < src.size() it throws IndexOutOfBoundsException: Source does not fit in dest. See the output in PECS in the Java standard library and the full method description in Collections.copy() method.
Comments