Já usamos o método collect() várias vezes para combinar os elementos retornados pelo Stream em um ArrayList. Esta é uma operação de redução, útil para converter uma coleção em outro tipo (geralmente uma coleção mutável). A função collect(), se usada em combinação com alguns métodos da classe de ferramenta Collectors, pode fornecer grande conveniência, como apresentaremos nesta seção.
Vamos continuar usando a lista Person anterior como exemplo para ver o que o método collect() pode fazer. Suponha que queiramos encontrar todas as pessoas com mais de 20 anos na lista original. Aqui está a versão implementada usando mutabilidade e o método forEach():
Copie o código do código da seguinte forma:
List<Pessoa> mais antiga que20 = new ArrayList<>();
.filter(pessoa -> pessoa.getAge() > 20)
.forEach(pessoa -> mais velhoThan20.add(pessoa)); System.out.println("Pessoas com mais de 20 anos: " + mais velhoThan20);
Usamos o método filter() para filtrar todas as pessoas com mais de 20 anos da lista. Então, no método forEach, adicionamos elementos a um ArrayList que foi inicializado anteriormente. Vamos primeiro dar uma olhada na saída deste código e depois reconstruí-lo.
Copie o código do código da seguinte forma:
Pessoas com mais de 20 anos: [Sara - 21, Jane - 21, Greg - 35]
A saída do programa está correta, mas ainda há um pequeno problema. Primeiro, adicionar elementos a uma coleção é uma operação de baixo nível – é imperativa, não declarativa. Se quisermos transformar esta iteração em simultânea, temos que considerar questões de segurança de thread - a variabilidade dificulta a paralelização. Felizmente, esse problema pode ser facilmente resolvido usando o método collect(). Vamos ver como isso é conseguido.
O método collect() aceita um Stream e os coleta em um contêiner de resultados. Para fazer isso, ele precisa saber três coisas:
+ Como criar um contêiner de resultados (por exemplo, usando o método ArrayList::new) + Como adicionar um único elemento ao contêiner (por exemplo, usando o método ArrayList::add) + Como mesclar um conjunto de resultados em outro (por exemplo, usando o método ArrayList: :addAll)
O último item não é necessário para operações seriais; o código foi projetado para suportar operações seriais e paralelas.
Fornecemos essas operações ao método collect e deixamos que ele colete o fluxo filtrado.
Copie o código do código da seguinte forma:
List<Pessoa> mais velha que20 =
pessoas.stream()
.filter(pessoa -> pessoa.getAge() > 20)
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println("Pessoas com mais de 20 anos: " +oldThan20);
O resultado deste código é o mesmo de antes, mas há muitas vantagens em escrevê-lo desta forma.
Em primeiro lugar, nosso método de programação é mais focado e expressivo, transmitindo claramente o propósito de coletar os resultados em um ArrayList. O primeiro parâmetro de collect() é uma fábrica ou produtor, e o parâmetro subsequente é uma operação usada para coletar elementos.
Segundo, como não estamos realizando modificações explícitas no código, podemos facilmente realizar essa iteração em paralelo. Deixamos a biblioteca subjacente lidar com as modificações e ela cuidará dos problemas de coordenação e segurança do thread, mesmo que o ArrayList em si não seja seguro para threads - bom trabalho.
Se as condições permitirem, o método collect() pode adicionar elementos a diferentes sublistas em paralelo e depois mesclá-los em uma lista grande de maneira segura para threads (o último parâmetro é usado para a operação de mesclagem).
Vimos que há muitos benefícios em usar o método collect() em vez de adicionar elementos manualmente a uma lista. Vejamos uma versão sobrecarregada deste método - é mais simples e conveniente - ele leva um Coletor como parâmetro. Este Coletor é uma interface que inclui produtores, somadores e combinadores. Nas versões anteriores, essas operações eram passadas para métodos como parâmetros independentes. O uso do Coletor é mais simples e pode ser reutilizado. A classe de ferramenta Collectors fornece um método toList que pode gerar uma implementação de Collector para adicionar elementos a um ArrayList. Vamos modificar o código anterior e usar o método collect().
Copie o código do código da seguinte forma:
List<Pessoa> mais velha que20 =
pessoas.stream()
.filter(pessoa -> pessoa.getAge() > 20)
.collect(Collectors.toList());
System.out.println("Pessoas com mais de 20 anos: " +oldThan20);
Uma versão concisa do método collect() da classe de ferramenta Coletores é usada, mas pode ser usada de mais de uma maneira. Existem vários métodos diferentes na classe de ferramenta Coletores para realizar diferentes operações de coleta e adição. Por exemplo, além do método toList(), há também o método toSet(), que pode ser adicionado a um Set, o método toMap(), que pode ser usado para coletar em um conjunto de valores-chave, e o método toMap(), que pode ser usado para coletar em um conjunto de valores-chave. método join(), que pode ser unido em uma string. Também podemos combinar métodos como mapeamento(),collectingAndThen(), minBy(), maxBy() e groupingBy() para uso.
Vamos usar o método groupingBy() para agrupar pessoas por idade.
Copie o código do código da seguinte forma:
Map<Integer, List<Person>> peopleByAge =
pessoas.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("Agrupado por idade: " + peopleByAge);
Basta chamar o método collect() para completar o agrupamento. groupingBy() aceita uma expressão lambda ou referência de método - isso é chamado de função de classificação - e retorna o valor de um determinado atributo do objeto que precisa ser agrupado. De acordo com o valor retornado pela nossa função, os elementos no contexto de chamada são colocados em um determinado grupo. Os resultados do agrupamento podem ser vistos na saída:
Copie o código do código da seguinte forma:
Agrupado por idade: {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]}
As pessoas foram agrupadas por idade.
No exemplo anterior agrupamos as pessoas por idade. Uma variante do método groupingBy() pode agrupar por múltiplas condições. O método groupingBy() simples usa um classificador para coletar elementos. O coletor groupingBy() geral pode especificar um coletor para cada grupo. Ou seja, os elementos passarão por diferentes classificadores e coleções durante o processo de coleta, como veremos a seguir.
Continuando com o exemplo acima, desta vez em vez de agrupar por idade, apenas pegamos os nomes das pessoas e classificamos por idade.
Copie o código do código da seguinte forma:
Mapa<Integer, List<String>> nameOfPeopleByAge =
pessoas.stream()
.coletar(
groupingBy(Person::getAge, mapeamento(Person::getName, toList())));
System.out.println("Pessoas agrupadas por idade: " + nameOfPeopleByAge);
Esta versão de groupingBy() aceita dois parâmetros: o primeiro é idade, que é a condição para agrupamento, e o segundo é um coletor, que é o resultado retornado pela função mapeando(). Todos esses métodos vêm da classe de ferramenta Collectors e são importados estaticamente neste código. O método mapeamento() aceita dois parâmetros, um é o atributo usado para mapeamento e o outro é o local onde os objetos serão coletados, como lista ou conjunto. Vamos dar uma olhada na saída do código acima:
Copie o código do código da seguinte forma:
Pessoas agrupadas por idade: {35=[Greg], 20=[John], 21=[Sara, Jane]}
Como você pode ver, os nomes das pessoas foram agrupados por idade.
Vejamos novamente uma operação de combinação: agrupe pela primeira letra do nome e selecione a pessoa mais velha de cada grupo.
Copie o código do código da seguinte forma:
Comparator<Pessoa> byAge = Comparator.comparing(Person::getAge);
Mapa<Caracter, Opcional<Pessoa>> mais antigaPersonOfEachLetter =
pessoas.stream()
.collect(groupingBy(pessoa -> pessoa.getName().charAt(0),
reduzindo(BinaryOperator.maxBy(byAge))));
System.out.println("Pessoa mais velha de cada letra:");
System.out.println(oldestPersonOfEachLetter);
Primeiro classificamos os nomes em ordem alfabética. Para conseguir isso, passamos uma expressão lambda como o primeiro parâmetro de groupingBy(). Esta expressão lambda é usada para retornar a primeira letra do nome para agrupamento. O segundo parâmetro não é mais mapeamento(), mas executa uma operação de redução. Dentro de cada grupo, ele usa o método maxBy() para derivar o elemento mais antigo de todos os elementos. A sintaxe parece um pouco inchada devido às muitas operações que combina, mas a coisa toda é assim: Agrupe pela primeira letra do nome e depois vá até a mais antiga do grupo. Considere a saída deste código, que lista a pessoa mais velha em um grupo de nomes que começam com uma determinada letra.
Copie o código do código da seguinte forma:
Pessoa mais velha de cada letra:
{S=Opcional[Sara - 21], G=Opcional[Greg - 35], J=Opcional[Jane - 21]}
Já experimentamos o poder do método collect() e da classe de utilitário Collectors. Na documentação oficial do seu IDE ou JDK, reserve um tempo para estudar a classe de ferramenta Collectors e se familiarizar com os vários métodos que ela fornece. A seguir usaremos expressões lambda para implementar alguns filtros.