Collect() Reunindo Elementos de Stream em uma Coleção
Você já está familiarizado com operações terminais e até mesmo as utilizou em exemplos e exercícios anteriores. Agora é o momento de analisar mais detalhadamente como elas funcionam. O primeiro método a ser abordado é o collect(), que é uma das principais operações terminais na Stream API.
O método collect()
É uma das ferramentas mais poderosas ao trabalhar com streams, permitindo acumular resultados em um List, Set ou Map, além de realizar agrupamentos complexos e cálculos estatísticos.
Existem duas implementações do método collect()—vamos explorar ambas.
Utilizando collect() com Interfaces Funcionais
O método collect() na Stream API pode ser utilizado com três interfaces funcionais para fornecer controle total sobre a coleta de dados:
Supplier<R> supplier– cria uma coleção vazia (R) onde os elementos serão armazenados. Por exemplo,ArrayList::newinicializa uma nova lista;BiConsumer<R, ? super T> accumulator– adiciona elementos do stream (T) à coleção (R). Por exemplo,List::addinsere itens em uma lista;BiConsumer<R, R> combiner– mescla duas coleções quando é utilizado processamento paralelo. Por exemplo,List::addAllcombina listas em uma só.
Os três componentes trabalham juntos para proporcionar flexibilidade na coleta de dados. Primeiro, o supplier cria uma coleção vazia que será utilizada para acumular os elementos do stream. Em seguida, o accumulator adiciona cada elemento conforme o stream os processa. Esse fluxo permanece simples em um stream sequencial.
No entanto, ao trabalhar com streams paralelos (parallelStream()), o processo se torna mais complexo.
O processamento de dados é distribuído entre múltiplas threads, com cada thread criando sua própria coleção separada. Após a conclusão do processamento, essas coleções individuais precisam ser mescladas em um único resultado. É nesse momento que o combiner atua, combinando de forma eficiente as partes separadas em uma coleção unificada.
Exemplo Prático
Você trabalha em uma loja online e possui uma lista de produtos. Sua tarefa é coletar apenas os produtos que custam mais de $500 utilizando o método collect() com três parâmetros.
Main.java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152package com.example; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // Initial list of products List<Product> productList = List.of( new Product("Laptop", 1200.99), new Product("Phone", 599.49), new Product("Headphones", 199.99), new Product("Monitor", 299.99), new Product("Tablet", 699.99) ); // Filtering and collecting products over $500 using `collect()` List<Product> expensiveProducts = productList.parallelStream() .filter(product -> product.getPrice() > 500) // Keep only expensive products .collect( ArrayList::new, // Create a new list (list, product) -> list.add(product), // Add each product to the list ArrayList::addAll // Merge lists (if the stream is parallel) ); // Print the result System.out.print("Products over $500: " + expensiveProducts); } } class Product { private String name; private double price; Product(String name, double price) { this.name = name; this.price = price; } public String getName() { return name; } public double getPrice() { return price; } @Override public String toString() { return name + " ($" + price + ")"; } }
O método collect() recebe três argumentos, cada um definindo uma etapa diferente na coleta de elementos em uma lista:
-
ArrayList::new(Supplier) → cria umArrayList<Product>vazio para armazenar os resultados; -
(list, product) -> list.add(product)(BiConsumer) → adiciona cadaProductà lista se atender à condição de filtro (price > 500); -
ArrayList::addAll(BiConsumer) → mescla múltiplas listas ao utilizar streams paralelos, garantindo que todos os produtos filtrados sejam combinados em uma única lista.
Embora o terceiro parâmetro seja utilizado principalmente para processamento paralelo, ele é obrigatório no collect().
Utilizando collect() com a Interface Collector
Além de trabalhar com três interfaces funcionais, o método collect() na Stream API também pode ser utilizado com implementações predefinidas da interface Collector.
Essa abordagem é mais flexível e conveniente, pois oferece métodos integrados para trabalhar com coleções.
A interface Collector<T, A, R> consiste em vários métodos principais:
Supplier<A> supplier()– cria um container vazio para acumular elementos;BiConsumer<A, T> accumulator()– define como os elementos são adicionados ao container;BinaryOperator<A> combiner()– mescla dois containers quando é utilizado processamento paralelo;Function<A, R> finisher()– transforma o container no resultado final.
Como pode ser observado, essa estrutura é semelhante ao método collect() que trabalha com interfaces funcionais, mas introduz o método finisher(). Essa etapa adicional permite um processamento extra nos dados coletados antes de retornar o resultado final—por exemplo, ordenar a lista antes de retorná-la.
Além disso, a interface Collector fornece o método characteristics(), que define propriedades que ajudam a otimizar a execução do stream:
Essas características ajudam a Stream API a otimizar o desempenho. Por exemplo, se uma coleção é inerentemente não ordenada, especificar UNORDERED pode evitar ordenações desnecessárias, tornando a operação mais eficiente.
Exemplo Prático
Imagine uma loja online em que seja necessário processar os preços dos produtos antes de coletá-los. Por exemplo, deseja-se arredondar cada preço para o número inteiro mais próximo, remover duplicatas e ordenar a lista final.
Main.java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package com.example; import java.util.*; import java.util.function.*; import java.util.stream.Collector; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<Double> prices = List.of(1200.99, 599.49, 199.99, 599.49, 1200.49, 200.0); // Using a custom `Collector` to round prices, remove duplicates, and sort List<Integer> processedPrices = prices.parallelStream() .collect(new RoundedSortedCollector()); System.out.println("Processed product prices: " + processedPrices); } } // Custom `Collector` that rounds prices, removes duplicates, and sorts them class RoundedSortedCollector implements Collector<Double, Set<Integer>, List<Integer>> { @Override public Supplier<Set<Integer>> supplier() { // Creates a `HashSet` to store unique rounded values return HashSet::new; } @Override public BiConsumer<Set<Integer>, Double> accumulator() { // Rounds price and adds to the set return (set, price) -> set.add((int) Math.round(price)); } @Override public BinaryOperator<Set<Integer>> combiner() { return (set1, set2) -> { set1.addAll(set2); // Merges two sets return set1; }; } @Override public Function<Set<Integer>, List<Integer>> finisher() { return set -> set.stream() .sorted() // Sorts the final list .toList(); } @Override public Set<Characteristics> characteristics() { // Order is not important during accumulation return Set.of(Characteristics.UNORDERED); } }
O processamento dos dados começa ao passá-los para um Collector personalizado chamado RoundedSortedCollector.
Este coletor acumula todos os preços em um Set<Integer>, garantindo que duplicatas sejam automaticamente removidas. Antes de adicionar cada valor, ele arredonda o preço usando Math.round(price) e converte para int. Por exemplo, tanto 1200.99 quanto 1200.49 se tornam 1200, enquanto 199.99 é arredondado para 200.
Se o stream for executado em modo paralelo, o método combiner() mescla dois conjuntos adicionando todos os elementos de um conjunto ao outro. Esta etapa é fundamental para ambientes multithread.
Na etapa final, após todos os preços serem coletados, o método finisher() transforma o conjunto em uma lista ordenada. Ele converte o Set<Integer> em um stream, aplica sorted() para organizar os valores em ordem crescente e, em seguida, coleta-os em uma List<Integer>.
Como resultado, obtém-se uma lista ordenada de preços únicos e arredondados que pode ser utilizada para cálculos ou exibição.
1. O que o método collect() faz na Stream API?
2. Qual capacidade adicional a interface Collector fornece em comparação ao uso de collect() com interfaces funcionais?
Obrigado pelo seu feedback!
Pergunte à IA
Pergunte à IA
Pergunte o que quiser ou experimente uma das perguntas sugeridas para iniciar nosso bate-papo
Can you explain the difference between using collect() with functional interfaces and with the Collector interface?
What are some common use cases for the collect() method in real-world applications?
Could you provide a simple code example demonstrating the use of collect() in Java?
Awesome!
Completion rate improved to 2.33
Collect() Reunindo Elementos de Stream em uma Coleção
Deslize para mostrar o menu
Você já está familiarizado com operações terminais e até mesmo as utilizou em exemplos e exercícios anteriores. Agora é o momento de analisar mais detalhadamente como elas funcionam. O primeiro método a ser abordado é o collect(), que é uma das principais operações terminais na Stream API.
O método collect()
É uma das ferramentas mais poderosas ao trabalhar com streams, permitindo acumular resultados em um List, Set ou Map, além de realizar agrupamentos complexos e cálculos estatísticos.
Existem duas implementações do método collect()—vamos explorar ambas.
Utilizando collect() com Interfaces Funcionais
O método collect() na Stream API pode ser utilizado com três interfaces funcionais para fornecer controle total sobre a coleta de dados:
Supplier<R> supplier– cria uma coleção vazia (R) onde os elementos serão armazenados. Por exemplo,ArrayList::newinicializa uma nova lista;BiConsumer<R, ? super T> accumulator– adiciona elementos do stream (T) à coleção (R). Por exemplo,List::addinsere itens em uma lista;BiConsumer<R, R> combiner– mescla duas coleções quando é utilizado processamento paralelo. Por exemplo,List::addAllcombina listas em uma só.
Os três componentes trabalham juntos para proporcionar flexibilidade na coleta de dados. Primeiro, o supplier cria uma coleção vazia que será utilizada para acumular os elementos do stream. Em seguida, o accumulator adiciona cada elemento conforme o stream os processa. Esse fluxo permanece simples em um stream sequencial.
No entanto, ao trabalhar com streams paralelos (parallelStream()), o processo se torna mais complexo.
O processamento de dados é distribuído entre múltiplas threads, com cada thread criando sua própria coleção separada. Após a conclusão do processamento, essas coleções individuais precisam ser mescladas em um único resultado. É nesse momento que o combiner atua, combinando de forma eficiente as partes separadas em uma coleção unificada.
Exemplo Prático
Você trabalha em uma loja online e possui uma lista de produtos. Sua tarefa é coletar apenas os produtos que custam mais de $500 utilizando o método collect() com três parâmetros.
Main.java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152package com.example; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // Initial list of products List<Product> productList = List.of( new Product("Laptop", 1200.99), new Product("Phone", 599.49), new Product("Headphones", 199.99), new Product("Monitor", 299.99), new Product("Tablet", 699.99) ); // Filtering and collecting products over $500 using `collect()` List<Product> expensiveProducts = productList.parallelStream() .filter(product -> product.getPrice() > 500) // Keep only expensive products .collect( ArrayList::new, // Create a new list (list, product) -> list.add(product), // Add each product to the list ArrayList::addAll // Merge lists (if the stream is parallel) ); // Print the result System.out.print("Products over $500: " + expensiveProducts); } } class Product { private String name; private double price; Product(String name, double price) { this.name = name; this.price = price; } public String getName() { return name; } public double getPrice() { return price; } @Override public String toString() { return name + " ($" + price + ")"; } }
O método collect() recebe três argumentos, cada um definindo uma etapa diferente na coleta de elementos em uma lista:
-
ArrayList::new(Supplier) → cria umArrayList<Product>vazio para armazenar os resultados; -
(list, product) -> list.add(product)(BiConsumer) → adiciona cadaProductà lista se atender à condição de filtro (price > 500); -
ArrayList::addAll(BiConsumer) → mescla múltiplas listas ao utilizar streams paralelos, garantindo que todos os produtos filtrados sejam combinados em uma única lista.
Embora o terceiro parâmetro seja utilizado principalmente para processamento paralelo, ele é obrigatório no collect().
Utilizando collect() com a Interface Collector
Além de trabalhar com três interfaces funcionais, o método collect() na Stream API também pode ser utilizado com implementações predefinidas da interface Collector.
Essa abordagem é mais flexível e conveniente, pois oferece métodos integrados para trabalhar com coleções.
A interface Collector<T, A, R> consiste em vários métodos principais:
Supplier<A> supplier()– cria um container vazio para acumular elementos;BiConsumer<A, T> accumulator()– define como os elementos são adicionados ao container;BinaryOperator<A> combiner()– mescla dois containers quando é utilizado processamento paralelo;Function<A, R> finisher()– transforma o container no resultado final.
Como pode ser observado, essa estrutura é semelhante ao método collect() que trabalha com interfaces funcionais, mas introduz o método finisher(). Essa etapa adicional permite um processamento extra nos dados coletados antes de retornar o resultado final—por exemplo, ordenar a lista antes de retorná-la.
Além disso, a interface Collector fornece o método characteristics(), que define propriedades que ajudam a otimizar a execução do stream:
Essas características ajudam a Stream API a otimizar o desempenho. Por exemplo, se uma coleção é inerentemente não ordenada, especificar UNORDERED pode evitar ordenações desnecessárias, tornando a operação mais eficiente.
Exemplo Prático
Imagine uma loja online em que seja necessário processar os preços dos produtos antes de coletá-los. Por exemplo, deseja-se arredondar cada preço para o número inteiro mais próximo, remover duplicatas e ordenar a lista final.
Main.java
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455package com.example; import java.util.*; import java.util.function.*; import java.util.stream.Collector; import java.util.stream.Stream; public class Main { public static void main(String[] args) { List<Double> prices = List.of(1200.99, 599.49, 199.99, 599.49, 1200.49, 200.0); // Using a custom `Collector` to round prices, remove duplicates, and sort List<Integer> processedPrices = prices.parallelStream() .collect(new RoundedSortedCollector()); System.out.println("Processed product prices: " + processedPrices); } } // Custom `Collector` that rounds prices, removes duplicates, and sorts them class RoundedSortedCollector implements Collector<Double, Set<Integer>, List<Integer>> { @Override public Supplier<Set<Integer>> supplier() { // Creates a `HashSet` to store unique rounded values return HashSet::new; } @Override public BiConsumer<Set<Integer>, Double> accumulator() { // Rounds price and adds to the set return (set, price) -> set.add((int) Math.round(price)); } @Override public BinaryOperator<Set<Integer>> combiner() { return (set1, set2) -> { set1.addAll(set2); // Merges two sets return set1; }; } @Override public Function<Set<Integer>, List<Integer>> finisher() { return set -> set.stream() .sorted() // Sorts the final list .toList(); } @Override public Set<Characteristics> characteristics() { // Order is not important during accumulation return Set.of(Characteristics.UNORDERED); } }
O processamento dos dados começa ao passá-los para um Collector personalizado chamado RoundedSortedCollector.
Este coletor acumula todos os preços em um Set<Integer>, garantindo que duplicatas sejam automaticamente removidas. Antes de adicionar cada valor, ele arredonda o preço usando Math.round(price) e converte para int. Por exemplo, tanto 1200.99 quanto 1200.49 se tornam 1200, enquanto 199.99 é arredondado para 200.
Se o stream for executado em modo paralelo, o método combiner() mescla dois conjuntos adicionando todos os elementos de um conjunto ao outro. Esta etapa é fundamental para ambientes multithread.
Na etapa final, após todos os preços serem coletados, o método finisher() transforma o conjunto em uma lista ordenada. Ele converte o Set<Integer> em um stream, aplica sorted() para organizar os valores em ordem crescente e, em seguida, coleta-os em uma List<Integer>.
Como resultado, obtém-se uma lista ordenada de preços únicos e arredondados que pode ser utilizada para cálculos ou exibição.
1. O que o método collect() faz na Stream API?
2. Qual capacidade adicional a interface Collector fornece em comparação ao uso de collect() com interfaces funcionais?
Obrigado pelo seu feedback!