O princípio PECS, acrônimo para "Producer Extends, Consumer Super", é uma diretriz fundamental no uso de genéricos em Java para otimizar a flexibilidade e a segurança de tipos. Essencialmente, ele define como utilizar wildcards em parâmetros de tipos genéricos: use <? extends T> quando o tipo genérico atua como um produtor de elementos do tipo T (ou de seus subtipos), e use <? super T> quando ele atua como um consumidor de elementos do tipo T (ou de seus supertipos).
Considere a seguinte API de exemplo para uma classe Stack genérica:
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
Imagine a necessidade de adicionar um método para empilhar sequenicalmente todos os elementos de uma coleção em uma pilha. Uma primeira abordagem poderia ser:
public void pushAll(Iterable<E> source) {
for (E element : source) {
push(element);
}
}
Agora, suponha que você tenha uma Stack<Number> e deseje alimentá-la com uma coleção de Integers:
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = getIntegerIterable(); // Suponha que isso retorne um Iterable de Integers
numberStack.pushAll(integers);
Este código não compilará. Embora Integer seja um subtipo de Number, um Iterable<Integer> não é um supertipo de um Iterable<Number>. Isso ocorre porque os tipos genéricos em Java são invariantes por padrão.
Para rseolver isso, Java oferece os wildcards limitados. Modificando o parâmetro do método pushAll para aceitar um iterável de qualquer tipo que seja um subtipo de E, resolvemos o problema:
public void pushAll(Iterable<? extends E> source) {
for (E element : source) {
push(element);
}
}
Aqui, <? extends E> é o uso de "producer-extends". O Iterable atua como um produtor de elementos. Ao usar <? extends E>, garantimos que o iterável pode conter quaisquer elementos que sejam E ou subtipos de E. Durante a iteração, cada elemento pode ser tratado com segurança como um E.
Por outro lado, considere um método popAll que remove elementos de uma pilha e os adiciona a uma coleção de destino:
public void popAll(Collection<E> destination) {
while (!isEmpty()) {
destination.add(pop());
}
}
Suponha que tenhamos uma Stack<Number> e uma Collection<Object>:
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = new ArrayList<Object>();
numberStack.popAll(objects);
Novamente, este código não compilará. A coleção objects é um consumidor, pois elementos serão adicionados a ela. Para permitir que a pilha (contendo Numbers) adicione seus elementos à coleção de objetos, usamos <? super E>:
public void popAll(Collection<? super E> destination) {
while (!isEmpty()) {
destination.add(pop());
}
}
Com Collection<? super E>, garantimso que a coleção de destino seja um supertipo de E. Isso permite que a pilha adicione com segurança seus elementos (que são E ou subtipos de E) à coleção, independentemente do tipo exato do supertipo.
Em resumo:
- Quando um tipo genérico é usado para produzir elementos (ou seja, para lê-los), utilize
<? extends T>. - Quando um tipo genérico é usado para consumir elementos (ou seja, para adicioná-los), utilize
<? super T>.
Este princípio é uma adaptação de conceitos apresentados em "Effective Java" por Joshua Bloch.
// Exemplo prático com código Java modificado
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
class SimpleStack<T> {
private List<T> elements = new ArrayList<>();
public void push(T item) {
elements.add(item);
}
public T pop() {
if (elements.isEmpty()) {
throw new IllegalStateException("Stack is empty");
}
return elements.remove(elements.size() - 1);
}
public boolean isEmpty() {
return elements.isEmpty();
}
// Producer example
public void pushAllItems(Iterable<? extends T> source) {
for (T item : source) {
this.push(item);
}
}
// Consumer example
public void popAllItems(Collection<? super T> destination) {
while (!this.isEmpty()) {
destination.add(this.pop());
}
}
}
public class PecsDemo {
public static void main(String[] args) {
// Producer Example: Stack<number> receiving Iterable<integer>
SimpleStack<Number> numberStackProducer = new SimpleStack<>();
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
integers.add(3);
System.out.println("--- Producer Test ---");
System.out.println("Before pushAll: numberStackProducer empty? " + numberStackProducer.isEmpty());
numberStackProducer.pushAllItems(integers); // Valid because Iterable extends Number>
System.out.println("After pushAll: numberStackProducer empty? " + numberStackProducer.isEmpty());
System.out.println("Popping from numberStackProducer (producer test): " + numberStackProducer.pop()); // Should be 3
// Consumer Example: Stack<integer> adding to Collection<number>
SimpleStack<Integer> integerStackConsumer = new SimpleStack<>();
integerStackConsumer.push(10);
integerStackConsumer.push(20);
List<Number> numberListConsumer = new ArrayList<>();
numberListConsumer.add(0.5);
System.out.println("\n--- Consumer Test ---");
System.out.println("Before popAll: numberListConsumer = " + numberListConsumer);
integerStackConsumer.popAllItems(numberListConsumer); // Valid because Collection super Integer>
System.out.println("After popAll: numberListConsumer = " + numberListConsumer); // Should contain 0.5, 20, 10
System.out.println("integerStackConsumer empty? " + integerStackConsumer.isEmpty());
}
}
</number></integer></integer></number>