PECS (Producer Extends, Consumer Super) is the Java generics rule that a parameter which produces values for our code uses the wildcard <? extends T>, and a parameter which consumes values from our code uses <? super T>.
We use PECS when we write a generic method that should accept more than one exact type. For example, a method that copies items should read from a List<Apple> and write into a List<Fruit> without any cast.
The following example copies apples into a fruit basket. The source list is a producer, so it uses extends, and the target list is a consumer, so it uses super.
static <T> void copy(List<? extends T> source, List<? super T> target) {
for (T item : source) {
target.add(item);
}
}
List<Apple> apples = new ArrayList<>(List.of(new Apple(), new AsianApple()));
List<Fruit> basket = new ArrayList<>();
copy(apples, basket); // basket = [I am an Apple !!, I am an AsianApple !!]
List<? extends Fruit> producer = apples;
Fruit fruit = producer.get(0); // I am an Apple !! (we can read a Fruit)
// producer.add(new Apple()); // compile error: we cannot write
List<? super Apple> consumer = basket;
consumer.add(new AsianApple()); // OK: we can write an Apple or a subtype
Object item = consumer.get(0); // I am an Apple !! (we can read only an Object)
Notice that each wildcard allows only one direction. A <? extends Fruit> list gives us Fruit objects but accepts no new elements, whereas a <? super Apple> list accepts apples but gives us only Object references.
Next, we see why Java generics need wildcards at all, and we look at each half of the rule with its compile errors. After that, we cover methods that read and write the same list, and the JDK methods that follow PECS.
1. Syntax
The JDK collection API has two methods for adding elements to a collection. They look alike, but the first one uses <? extends E> and the second one uses <? super T>.
The method Collection.addAll() adds all members of the collection c into the collection on which we call the method.
boolean addAll(Collection<? extends E> c);
The method Collections.addAll() adds the given elements to the collection c.
public static <T> boolean addAll(Collection<? super T> c, T... elements);
In Collection.addAll(), the parameter c is the source, and the method reads from it, so c is a producer and uses extends. In Collections.addAll(), the parameter c is the target, and the method writes into it, so c is a consumer and uses super. The PECS rule describes the difference between the two parameters. Joshua Bloch introduced the term in Effective Java (Item 31 of the third edition, “Use bounded wildcards to increase API flexibility”).
List<Apple> appleList = List.of(new Apple());
List<Fruit> all = new ArrayList<>();
all.addAll(appleList); // [I am an Apple !!]
List<Fruit> target = new ArrayList<>();
boolean changed = Collections.addAll(target, new Apple(), new AsianApple()); // true
2. Generics with Wildcards
Java generics give us type safety, and generic types are invariant. Invariant means that a List<Apple> is not a List<Fruit>, even though Apple is a subclass of Fruit. If we declare a List<Integer>, the compiler reports any attempt to insert a non-integer value into that list.
List<Apple> apples = new ArrayList<>();
List<Fruit> fruits = apples; // compile error: List<Apple> cannot be converted to List<Fruit>
The compiler rejects the assignment for a good reason. If it were allowed, we could call fruits.add(new Fruit()), and a plain Fruit would end up in a list that the rest of the code reads as apples.
Arrays work the other way, because they are covariant, which means that an Apple[] is also a Fruit[]. The compiler accepts the same mistake with arrays, and the JVM catches it only at runtime with an ArrayStoreException.
Fruit[] fruitArray = new Apple[1];
fruitArray[0] = new Fruit(); // ArrayStoreException: com.howtodoinjava.core.generics.pecs.Fruit
Invariance keeps generic collections safe, but it also makes methods rigid. Say a fruit shop app has a method printAll(List<Fruit> fruits). The method only reads the list, yet it rejects a List<Apple>. Wildcards let such a method accept a List<Apple> too.
- The upper-bounded wildcard <? extends Fruit> means “a list of some unknown subtype of Fruit“. The list type is covariant, so List<Apple> fits.
- The lower-bounded wildcard <? super Apple> means “a list of some unknown supertype of Apple“. The list type is contravariant, so List<Fruit> and List<Object> fit.

3. Understanding ‘Producer Extends’ or <? extends T>
This is the first part of PECS, i.e. PE (Producer extends). A fruit shop app keeps fruits in baskets, which are collections of fruits. When we take fruit out of a basket, we want to be sure that we get a Fruit and nothing else, so we can write code such as Fruit fruit = basket.get(0).
The example has a small class hierarchy. Apple extends Fruit, and AsianApple extends Apple. Each class overrides toString(), so we can see which object we print.
class Fruit {
@Override
public String toString() {
return "I am a Fruit !!";
}
}
class Apple extends Fruit {
@Override
public String toString() {
return "I am an Apple !!";
}
}
class AsianApple extends Apple {
@Override
public String toString() {
return "I am an AsianApple !!";
}
}
We assign a list of apples to a basket of type List<? extends Fruit>. The assignment compiles, because Apple is a subtype of Fruit, and every element we read from the basket is a Fruit.
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? extends Fruit> basket = apples;
for (Fruit fruit : basket) {
System.out.println(fruit); // I am an Apple !!
}
// basket.add(new Apple()); // compile error
// basket.add(new Fruit()); // compile error
The last two lines don’t compile, and the compiler reports the following error for basket.add(new Apple()).
error: incompatible types: Apple cannot be converted to CAP#1
CAP#1 extends Fruit from capture of ? extends Fruit
The <? extends Fruit> wildcard tells the compiler that the list holds some subtype of Fruit, but we cannot know which subtype, as there may be many. The variable basket can point to a List<AsianApple>, and adding an Apple to it would break that list. The compiler cannot check it, so it allows no add() call at all, apart from add(null), because null fits every type. The name CAP#1 in the message is the compiler’s name for that unknown type (a capture).
On the other hand, whatever the exact type is, it is a subtype of Fruit, so we can get data out of the list with the guarantee that it is a Fruit.
A method that only reads its parameter can therefore accept lists of any fruit type. The method printAll() compiles once and works for all three classes.
static void printAll(Collection<? extends Fruit> fruits) {
for (Fruit fruit : fruits) {
System.out.println(fruit);
}
}
printAll(List.of(new Apple())); // I am an Apple !!
printAll(List.of(new AsianApple())); // I am an AsianApple !!
printAll(List.of(new Fruit())); // I am a Fruit !!
In the example, we take elements out of the collection List<? extends Fruit> basket, so the basket produces the elements. When we only read elements from a collection, it is a producer, and we declare it with <? extends T>.
4. Understanding ‘Consumer Super’ or <? super T>
Next, we look at the same basket from the other side. Say we write a method that only adds apples to a basket, like the method addAll(Collection<? super T> c, T… elements) from section 1. The basket stores the elements, so it is a consumer of elements.
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
List<? super Apple> basket = apples;
basket.add(new Apple()); // OK
basket.add(new AsianApple()); // OK
// basket.add(new Fruit()); // compile error: Fruit cannot be converted to CAP#1
Object first = basket.get(0); // I am an Apple !!
// Apple apple = basket.get(0); // compile error: CAP#1 cannot be converted to Apple
We can add apples and Asian apples to the basket, but we cannot add a Fruit, which is a supertype of Apple. The reason is that basket refers to a list of something that is a supertype of Apple. We don’t know which supertype it is, but every possible list, from List<Apple> up to List<Object>, accepts an Apple and any subtype of Apple. A Fruit fits only some of the lists, so the compiler rejects it.
When we read from such a list, the only type that fits every possible supertype of Apple is Object, so get() returns an Object reference. To use the value as an Apple, we would need a cast, which is the type check that generics should do for us.
A method that only writes into its parameter can accept a list of Apple or of any supertype. For example, a shop method addApples() fills a List<Fruit> for the fruit counter or a List<Object> for a mixed delivery.
static void addApples(Collection<? super Apple> basket, int count) {
for (int i = 0; i < count; i++) {
basket.add(new Apple());
}
}
List<Object> objects = new ArrayList<>();
addApples(objects, 2); // [I am an Apple !!, I am an Apple !!]
List<Fruit> fruitBasket = new ArrayList<>();
addApples(fruitBasket, 1); // [I am an Apple !!]
In the example, we put elements into the collection List<? super Apple> basket, so the basket consumes the elements. When we only add elements to a collection, it is a consumer, and we declare it with <? super T>.
5. Producer and Consumer in One Method
Many methods have one parameter of each kind, so they use both wildcards. The JDK method Collections.copy() reads from src and writes into dest, and our copy() method in the intro follows the same signature.
public static <T> void copy(List<? super T> dest, List<? extends T> src)
Comparators are a common consumer in the JDK. A Comparator<Fruit> takes any two fruits, so it can also compare two apples. For this reason, List.sort() accepts a Comparator<? super E>, and Collections.max() uses both wildcards.
Comparator<Fruit> byText = Comparator.comparing(Fruit::toString);
List<Apple> mixed = new ArrayList<>(List.of(new AsianApple(), new Apple()));
mixed.sort(byText); // [I am an Apple !!, I am an AsianApple !!]
Apple max = Collections.max(mixed, byText); // I am an AsianApple !!
The JDK declares Collections.max() as <T> T max(Collection<? extends T> coll, Comparator<? super T> comp). The collection produces the elements, and the comparator consumes them.
The Stream.map() method follows the same pattern with Function<? super T, ? extends R>, because the function consumes a stream element and produces a result.
6. When Not to Use a Wildcard
When a method both reads from and writes into the same collection, we use a type parameter such as List<T> and no wildcard. For example, a method that swaps the first two elements reads two values and writes them back, so neither extends nor super works.
static <T> void swapFirstTwo(List<T> list) {
T first = list.get(0);
list.set(0, list.get(1));
list.set(1, first);
}
List<String> words = new ArrayList<>(List.of("a", "b", "c"));
swapFirstTwo(words); // [b, a, c]
We also don’t use a bounded wildcard as a return type. A method that returns List<? extends Fruit> forces every caller to work with wildcard types, so we return List<Fruit> or a List<T> instead. Wildcards belong on the input parameters, where they make a method accept more types.
7. PECS FAQs
7.1. What Is the Difference Between <? extends T> and <T extends Fruit>?
The wildcard <? extends T> is the type of a parameter or a variable, and the code never gets a name for the exact type. The declaration <T extends Fruit> declares a type parameter with an upper bound, and the method can use T in more than one place, e.g. <T extends Fruit> void addTwice(List<T> list, T item). When the type appears only once in a signature, a wildcard is enough.
7.2. Is List<?> the Same as List<Object>?
No. A List<Object> accepts any object in add(), but it does not accept a List<String> as an argument, because generics are invariant. A List<?> (unbounded wildcard) accepts a list of any type, but we can add only null to it, and any.add(“x”) fails with “String cannot be converted to CAP#1”. We use List<?> when the method needs only the methods of Object or the size of the list.
8. Conclusion
PECS tells us which wildcard to put on a generic parameter. A parameter that produces values for our code uses <? extends T>, so the method accepts collections of T and of any subtype. A parameter that consumes values from our code uses <? super T>, so the method accepts collections of T and of any supertype.
Each wildcard allows one direction. We cannot add to an extends collection, and we read only Object from a super collection. When a method reads and writes the same collection, we use a plain type parameter without a wildcard.
The JDK follows the rule everywhere, for example in Collections.copy(), Collections.max(), List.sort() and Stream.map(), so reading these signatures with PECS in mind shows us which parameter is read and which one is written.
9. References
- Guidelines for Wildcard Use (The Java Tutorials)
- Upper Bounded Wildcards (The Java Tutorials)
- Lower Bounded Wildcards (The Java Tutorials)
- Collections JavaDoc (Java 25)
- Wildcards in the Java Language Specification (Java SE 25)
Happy Learning !!