Funcionalidades do JDK 8 (Java 8) - Parte X - API de Streams
Aplicação da API de Streams Parte II
Gerando mapas com coletores
Podemos gerar um stream com todas as linhas dos arquivos que lemos de um determinado diretório:
Stream<String> stream = Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.flatMap(path -> lines(path));
Podemos ainda ter um stream com a quantidade de linhas de cada arquivo, basta em vez de usar o flatMap()
utilizar um map()
para a quantidade de linhas, usando o count()
, veja:
List<Long> linhas = Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.map(path -> lines(path).count())
.collect(Collectors.toList());
Agora e se desejarmos saber quantas linhas tem cada arquivo nesse diretório ? Podemos fazer um forEach()
e popular um Map<Path, Long>
no qual a chave é o arquivo e o valor é a quantidade de linhas dele:
Map<Path, Long> linesInFile = new HashMap<>();
Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> linesInFile.put(path, lines(path).count()));
Esse código vai produzir uma saída mais ou menos assim:
{/home/seujorge/meu_arquivo.txt=52}
Isso já é bom, já que sob esse approach nos livramos de coisas com oBufferedReaders
e loops, porém essa solução não é muito funcional, note que a lambda passada para o forEach()
utiliza uma variável que está declarada fora do seu escopo, assim a lambda altera o estado dessa variável gerando efeitos colaterais, o que afetaria coisas como paralelismo, para resolver esse problema podemos fazer da seguinte forma:
Map<Path, Long> linesInFile = Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.collect(Collectors.toMap(path -> path, path -> lines(path).count()));
O toMap()
recebe duas Functions
. A primeira produzirá a chave (o path) e a segunda o valor (quantidade de linhas), é bem comum precisarmos de uma lambda que retorna o próprio argumento, como fazemos em path -> path,
, nesse caso podemos utilizar Function.identity()
para deixar mais claro:
Map<Path, Long> linesInFile = Files.list(Paths.get(ROOT_PATH))
.filter(path -> path.toString().endsWith(".txt"))
.collect(Collectors.toMap(Function.identity(), path -> lines(path).count()));
Sendo assim saiba que sempre que uma lambda tiver um arquivo tipo algumaCoisa -> algumaCoisa
, você pode utilizar Function.identity()
.
Outro exemplo de uso para Function.identity()
, imagina que queremos mapear todos os pilotos utilizando seus nomes como chave:
A princípio teríamos o seguinte código:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 122, false);
Piloto gasly = new Piloto("Gasly", 180, false);
Piloto leclerc = new Piloto("Leclerc", 789, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 967, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
Map<String, Piloto> nomes = pilotos.stream()
.collect(Collectors.toMap(Piloto::getNome, piloto -> piloto));
System.out.println(nomes);
}
}
Note que passamos ao toMap()
um argumento piloto -> piloto
, podemos alterar esse argumento para Function.identity()
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 122, false);
Piloto gasly = new Piloto("Gasly", 180, false);
Piloto leclerc = new Piloto("Leclerc", 789, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 967, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
Map<String, Piloto> nomes = pilotos.stream()
.collect(Collectors.toMap(Piloto::getNome, Function.identity()));
System.out.println(nomes);
}
}
Se a classe Piloto
fosse uma entidade JPA poderíamos mapear o id e o nome, assim:
pilotos.stream()
.collect(Collectors.toMap(Piloto::getId, Function.identity()));
Agrupando e particionando
Temos na classe Piloto
um booleano para dizer se o piloto é ou não um campeão mundial, vamos revistar a classe para lembrar
package br.com.jorgerabellodev.streams.model;
public class Piloto {
private String nome;
private int pontuacao;
private boolean campeaoMundial;
public Piloto(String nome, int pontuacao) {
this.nome = nome;
this.pontuacao = pontuacao;
}
public Piloto(String nome, int pontuacao, boolean campeaoMundial) {
this.nome = nome;
this.pontuacao = pontuacao;
this.campeaoMundial = campeaoMundial;
}
public String getNome() {
return nome;
}
public int getPontuacao() {
return pontuacao;
}
public boolean isCampeaoMundial() {
return campeaoMundial;
}
public void tornarCampeaoMundial() {
this.campeaoMundial = true;
}
@Override
public String toString() {
return "Piloto{" +
"nome='" + nome + '\'' +
", pontuacao=" + pontuacao +
", campeaoMundial=" + campeaoMundial +
'}';
}
}
Agora vamos criar alguns pilotos
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 100, false);
Piloto gasly = new Piloto("Gasly", 100, false);
Piloto leclerc = new Piloto("Leclerc", 289, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 1000, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
System.out.println(pilotos);
}
}
Agora queremos um mapa em que a chave seja a pontuação do piloto e o valor seja uma lista de pilotos que possuem aquela pontuação, ou seja queremos um Map<Integer, List<Piloto>
. Com Java 8 nossa implementação vai ficar mais ou menos assim:
Map<Integer, List<Piloto>> pontuacoesPorPilotos = new HashMap<>();
for (Piloto piloto : pilotos) {
pontuacoesPorPilotos.computeIfAbsent(piloto.getPontuacao(), driver -> new ArrayList<>()).add(piloto);
}
O código todo fica assim:
import java.util.*;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 100, false);
Piloto gasly = new Piloto("Gasly", 100, false);
Piloto leclerc = new Piloto("Leclerc", 289, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 1000, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
Map<Integer, List<Piloto>> pontuacoesPorPilotos = new HashMap<>();
for (Piloto piloto : pilotos) {
pontuacoesPorPilotos.computeIfAbsent(piloto.getPontuacao(), driver -> new ArrayList<>()).add(piloto);
}
System.out.println(pontuacoesPorPilotos);
}
}
Até o Java 7 precisaríamos de muito mais código para executar essa mesma ação.
O método computeIfAbsent()
chama a Function
da lambda no caso de não encontrar um valor para a chave piloto.getPontuacao()
e associar o resultado (a nova ArrayList) a essa mesma chave, essa chamada para computeIfAbsent()
faz o papel de um if que seria necessário nesse fluxo.
Esse código funciona, mas queremos trabalhar com streams, então poderíamos pensar em escrever um Collector
ou trabalhar com o reduce()
como já vimos, porém existe um Collector que faz exatamente isso:
Assim teríamos um mapa com as pontuações
Map<Integer, List<Piloto>> pontuacoesPorPilotos = pilotos.stream()
.collect(Collectors.groupingBy(Piloto::getPontuacao));
Veja que temos o mesmo resultado do código anterior mas com menos código.
Agora imagina que queremos particionar os pilotos em campeões mundiais e não campeões mundiais. Para essa finalidade existe o partitioningBy()
Map<Boolean, List<String>> campeoesENaoCampeoes = pilotos.stream()
.collect(Collectors.partitioningBy(Piloto::isCampeaoMundial, Collectors.mapping(Piloto::getNome, Collectors.toList())));
O código todo seria assim
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 100, false);
Piloto gasly = new Piloto("Gasly", 100, false);
Piloto leclerc = new Piloto("Leclerc", 289, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 1000, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
Map<Boolean, List<String>> campeoesENaoCampeoes = pilotos.stream()
.collect(Collectors.partitioningBy(Piloto::isCampeaoMundial, Collectors.mapping(Piloto::getNome, Collectors.toList())));
System.out.println(campeoesENaoCampeoes);
}
}
Vamos tentar somar todas as pontuações dos campeões mundiais e dos campeões mundiais, separadamente, podemos fazer isso facilmente da seguinte forma:
Map<Boolean, Integer> pontuacoes = pilotos.stream()
.collect(Collectors.partitioningBy(Piloto::isCampeaoMundial, Collectors.summingInt(Piloto::getPontuacao)));
Note que até o momento não utilizamos mais loops e mesmo se precisarmos concatenar os nomes dos usuários também seria fácil:
Imagina que queremos a seguinte saída
Prost, Gasly, Leclerc, Senna, Albon, Tsunoda, Bottas, Hamilton, Verstappen
Poderíamos obte-la facilmente assim:
String nomesDosPilotos = pilotos.stream()
.map(Piloto::getNome)
.collect(Collectors.joining(", "));
Com streams e collectors podemos escrever código mais enxuto em estilo funcional e consequentemente mais expressivo. Além disso produzimos menos efeitos colaterais favorecendo a imutabilidade das coleções e facilitando a paralelização.
Executando o pipeline em paralelo
Vamos voltar ao exemplo em que precisamos filtrar os pilotos com mais de 900 pontos
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 100, false);
Piloto gasly = new Piloto("Gasly", 100, false);
Piloto leclerc = new Piloto("Leclerc", 289, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 1000, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
List<Piloto> pilotosComMaisDe900Pontos = pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.sorted(Comparator.comparing(Piloto::getNome))
.collect(Collectors.toList());
System.out.println(pilotosComMaisDe900Pontos);
}
}
Nesse exemplo tudo acontece na mesma thread, se tivermos uma lista muito grande o processo poderá levar muito tempo travando essa thread até que termine sua execução. Seria interessante paralelizar esse processo, visto que atualmente a maioria dos dispositivos, se não todos eles, possuem mais de um processador, ou núcleo de processador, porém escrever código que use Thread para filtrar, ordenar e coletar pode ser trabalhoso. Poderíamos trabalhar com Fork/Join
mas mesmo assim teríamos bastante trabalho.
A boa notícia é que as collections oferecem uma implementação de stream diferente, o stream paralelo. Esse stream tem capacidade para decidir quantas threads deve utilizar, como deve quebrar o processamento de dados e qual será a forma de unir o resultado.
Para utilizar basta no lugar de invocar o método stream()
invocar parallelStream()
List<Piloto> pilotosComMaisDe900Pontos = pilotos.parallelStream()
.filter(piloto -> piloto.getPontuacao() > 900)
.sorted(Comparator.comparing(Piloto::getNome))
.collect(Collectors.toList());
Sendo assim nosso código ficaria assim:
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
Piloto prost = new Piloto("Prost", 100, false);
Piloto gasly = new Piloto("Gasly", 100, false);
Piloto leclerc = new Piloto("Leclerc", 289, false);
Piloto senna = new Piloto("Senna", 1000, true);
Piloto albon = new Piloto("Albon", 562, false);
Piloto tsunoda = new Piloto("Tsunoda", 967, false);
Piloto bottas = new Piloto("Bottas", 609, false);
Piloto hamilton = new Piloto("Hamilton", 1000, true);
Piloto verstappen = new Piloto("Verstappen", 890, true);
List<Piloto> pilotos = Arrays.asList(prost, gasly, leclerc, senna, albon, tsunoda, bottas, hamilton, verstappen);
List<Piloto> pilotosComMaisDe900Pontos = pilotos.parallelStream()
.filter(piloto -> piloto.getPontuacao() > 900)
.sorted(Comparator.comparing(Piloto::getNome))
.collect(Collectors.toList());
System.out.println(pilotosComMaisDe900Pontos);
}
}
Para esse pequeno exemplo, não faz muita diferença mas em sistemas distribuídos grandes, com uma alta volumetria de dados a performance agradeceria !
Espero que este artigo tenha sido útil e que quem leu tenha, assim como eu, compreendido melhor a API de streams.
A nossa última parte vai falar sobre a API de datas !