O Princípio PECS em Generics Java: Produtor estende, Consumidor super

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>

Tags: java generics wildcards pecs extends

Publicado em 6-18 23:17