Explain Codes LogoExplain Codes Logo

What is PECS (Producer Extends Consumer Super)?

java
type-substitution
liskov-substitution-principle
generics
Nikita BarsukovbyNikita Barsukov·Mar 11, 2025
TLDR

PECS stands for Producer Extends, Consumer Super. It's the golden rule for using wildcards in Java Generics for maximum flexibility and type safety. The extends keyword is used when your collection is intended to produce objects. Meanwhile, super is applied when your collection is "consuming" or accepting elements. Here's a quick look:

// For producing: Read 'T' items from a list - Use extends <T> T getItem(List<? extends T> list) { return list.get(0); } // For consuming: Add 'T' items to a list - Use super <T> void addItem(List<? super T> list, T item) { list.add(item); }

Succinctly, the use of extends is for reading, while super is for writing to a collection.

Now, let's extend our knowledge and supercharge your understanding of PECS!

Concrete understanding of PECS

Safe reading with extends

When we're producing or retrieving objects, we use extends. This guarantees that it's safe to read elements from a collection consisting of T or any of its subtypes.

void processNumbers(List<? extends Number> numbers) { for (Number number : numbers) { // Safe to read 'Number' and laugh all the way to the bank } }

Safe writing with super

When we're adding or consuming objects, we lean on super. This allows us to put an object of type T or its supertypes safely into a collection.

void addIntegers(List<? super Integer> list) { // Add Integer (and let the list crunch the number) list.add(42); }

The in-between: bidirectional collections

For bidirectional collections—we read and write—skip the extends and super wildcards. They make a collection either read-only or write-only. Use a plain type parameter to preserve flexibility:

<T> void processList(List<T> list, T item) { T firstElement = list.get(0); // Safe read list.add(item); // Safe write }

Java abides by the Liskov Substitution Principle (LSP). This rule states that subtypes must be substitutable for their supertype, which is ensured by extends and super.

Bounding with wildcards

Bounded wildcards (? extends T and ? super T) introduce upper and lower bounds, making your generic methods work with a type that is a subtype or supertype of T.

PECS in action: Practical scenarios

Reading from covariant collections

When only reading elements, extends is your keyword. You're safe to retrieve elements from a collection filled with T or a subtype.

// Read-only mission with 'T' or its subtypes <T> T readFirstElement(List<? extends T> list) { return list.get(0); // Safe retrieval (and no rescue mission needed!) }

Writing into contravariant collections

When adding elements, super comes to play. You can add an object of type T or its supertypes to a collection.

// Insert 'T' or its subtypes into list <T> void addToCollection(List<? super T> list, T item) { list.add(item); // Safe addition. Added item? Check! }

Handling invariance

Lastly, invariance means that only T can be used, not its subtypes or supertypes.

// Classy handling of MyClass, no extends or super void handleMyClass(List<MyClass> list) { // Read-write operations on 'MyClass' type }

Smart practices

Constraints of wildcards

Wildcards (?) represent an unknown type. Collections equipped with them can be either consumers or producers, but not both.

Fetching with surgical precision

Retrieve items from a collection with extends and you'll always get the right type:

// Generic retrieval with 'extends', like picking the ace from a deck of cards <T> T smartFetch(List<? extends T> list) { return list.get(0); }

Wide acceptance with super

Add items to a collection with super, and it's like a warm invitation to a host of types:

// Welcoming 'T' or its subtypes to the party <T> void warmWelcome(List<? super T> list, T item) { list.add(item); // You're in! }