Back to Blog
Java

Java PECS Principle: Producer Extends, Consumer Super

java pecs principle: Understand the Java PECS principle for wildcards in generics. See how producer extends and consumer super prevent type errors in collection operat...

Java GenericsWildcardsType SafetyCollectionsAPI Design
Diagram illustrating the Java PECS principle with producer extends and consumer super wildcards.

When you use wildcards in Java generics, the compiler enforces rules that often surprise developers. The Java PECS principle—Producer Extends, Consumer Super—explains those rules and tells you which wildcard to use based on whether you are reading from or writing to a collection. Without it, you end up with code that either fails to compile or forces unsafe casts.

The Problem: Wildcards in Java Generics

Wildcards (?) let you write flexible generic code that works with a family of types. For example, a method that accepts List<? extends Number> can take a List<Integer>, List<Double>, or List<Number>. That flexibility comes with restrictions. The compiler cannot know the exact type of elements in a List<? extends Number>, so it prevents operations that would assume a specific type.

Consider this common mistake:

List<? extends Number> numbers = new ArrayList<Integer>(); numbers.add(42); // Compilation error

The compiler rejects the add call because the actual element type could be Double or BigDecimal. Adding an Integer would break type safety. The wildcard ? extends Number is a producer: it provides values, but you cannot put values into it.

What PECS Stands For

PECS is a mnemonic that guides wildcard selection based on the direction of data flow:

  • Producer Extends – Use ? extends T when the collection produces elements (you read from it).
  • Consumer Super – Use ? super T when the collection consumes elements (you write to it).

The name comes from the idea that a producer supplies items, so you can only get items out. A consumer accepts items, so you can only put items in. This asymmetry is enforced by the compiler to preserve type safety.

Producer Extends: Reading from a Collection

When you need to read elements from a collection and process them, ? extends T is the right choice. It allows the collection to hold any subtype of T, and you can safely treat every element as a T.

public static double sum(List<? extends Number> numbers) { double total = 0.0; for (Number n : numbers) { total += n.doubleValue(); } return total; }

Here, sum accepts List<Integer>, List<Double>, or any list whose element type is a subtype of Number. Inside the loop, each element is accessed as Number, so you can call doubleValue(). The compiler knows that every element is a Number because the wildcard guarantees that the actual type is a subtype of Number.

You cannot add elements to a List<? extends Number> because the exact element type is unknown. If you try, the compiler rejects the operation. This is not a limitation; it is a safety guarantee. The producer wildcard is designed for read-only access.

Consumer Super: Writing to a Collection

When you need to put elements into a collection, use ? super T. This wildcard allows the collection to hold T or any supertype of T. You can safely add a T because the collection is guaranteed to accept at least T.

public static void addIntegers(List<? super Integer> list) { list.add(1); list.add(2); }

This method can accept List<Integer>, List<Number>, or List<Object>. Adding an Integer is safe because all these types can hold an Integer. However, reading from a List<? super Integer> is problematic. The actual element type could be Object, so you cannot assign an element to an Integer without a cast. The consumer wildcard is designed for write-only access.

The following table summarizes the behavior:

WildcardCan Read as T?Can Write T?Typical Use
? extends TYesNoReading a collection
? super TNoYesWriting to a collection

Common Wildcard Mistakes

The most frequent error is using ? extends when you need to write, or ? super when you need to read. Another mistake is using a wildcard when a concrete type would be simpler.

Consider a method that copies elements from one list to another. A naive signature might use ? extends for both parameters:

public static void copy(List<? extends Number> src, List<? extends Number> dest) { for (Number n : src) { dest.add(n); // Error: cannot add Number to ? extends Number } }

The destination must be a consumer, so it should use ? super Number. The correct signature is:

public static void copy(List<? extends Number> src, List<? super Number> dest) { for (Number n : src) { dest.add(n); } }

This matches the standard Collections.copy signature, which uses ? extends T for the source and ? super T for the destination. The PECS principle applies to both parameters independently.

Another common mistake is using an unbounded wildcard List<?> when you need to read and write. An unbounded wildcard is neither a producer nor a consumer; you can only read Object values and cannot add anything except null. If you need both read and write, use a concrete type parameter instead.

Practical Use Cases in Real Code

PECS appears in many standard library methods. For example, Collections.max takes a Collection<? extends T> because it only reads elements. Collections.copy takes a List<? super T> for the destination because it writes. When designing your own generic methods, follow the same pattern.

A custom utility method that fills a list with a value is a classic consumer example:

public static <T> void fill(List<? super T> list, T value) { for (int i = 0; i < list.size(); i++) { list.set(i, value); } }

The list is a consumer of T values, so ? super T is correct. You can call fill with a List<Object> and a String, or with a List<Number> and an Integer.

For a producer example, consider a method that finds the maximum element in a collection:

public static <T extends Comparable<T>> T max(Collection<? extends T> coll) { T max = null; for (T item : coll) { if (max == null || item.compareTo(max) > 0) { max = item; } } return max; }

The collection produces T values, so ? extends T allows passing a Collection<Integer> when T is Number, as long as Integer implements Comparable<Integer>.

Maintainability and Runtime Behavior

Generics are erased at runtime, so wildcards have no runtime overhead. The PECS principle is purely a compile-time safety mechanism. Using it correctly makes your code more maintainable because the compiler catches type errors before they reach production.

When you follow PECS, your method signatures communicate intent. A parameter declared as List<? extends T> tells callers that the method will not modify the list. A parameter declared as List<? super T> signals that the method will add elements. This improves readability and reduces the chance of accidental mutation.

There is a tradeoff: using wildcards can make signatures harder to read for developers unfamiliar with PECS. In simple cases, a concrete type parameter like List<T> may be clearer. Use wildcards when you need to accept a broader range of types, but avoid them when they add complexity without benefit.

One limitation is that PECS does not apply when you need both read and write access to the same collection. In that case, use a concrete type parameter or an explicit type bound. For example, a method that swaps two elements in a list needs to read and write, so it should use List<T> rather than a wildcard.

Another edge case is when you have a collection that is both a producer and a consumer, such as a List<T> passed to a method that reads and writes. The wildcard cannot express this dual role. Use a type parameter instead, and let the caller decide the concrete type.

In practice, PECS is most valuable in library APIs where flexibility matters. Application code often uses concrete types, and wildcards are unnecessary. When you do use wildcards, let PECS be your guide, and the compiler will enforce the correct behavior.

java pecs principle: Practical Usage and Code Examples | RYUSLOG DEV