Funcionalidades do JDK 8 (Java 8) - Parte VII - API de Streams
API de Streams - Parte III
Ordenacao
Se precisarmos ordenar uma lista de pilotos por nome, sabemos como fazer:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
public class Main {
public static void main(String[] args) {
BiFunction<String, Integer, Piloto> criadorDePilotos = Piloto::new;
List<Piloto> pilotos = new ArrayList<>();
pilotos.add(criadorDePilotos.apply("Senna", 1000));
pilotos.add(criadorDePilotos.apply("Prost", 122));
pilotos.add(criadorDePilotos.apply("Gasly", 980));
pilotos.add(criadorDePilotos.apply("Leclerc", 789));
pilotos.add(criadorDePilotos.apply("Albon", 562));
pilotos.add(criadorDePilotos.apply("Tsunoda", 967));
pilotos.add(criadorDePilotos.apply("Bottas", 609));
pilotos.add(criadorDePilotos.apply("Hamilton", 967));
pilotos.add(criadorDePilotos.apply("Verstappen", 890));
pilotos.sort(Comparator.comparing(Piloto::getNome));
System.out.println(pilotos);
}
}
E quanto a um stream ? Vamos imaginar que queremos filtrar os pilotos com mais de 900 pontos e então orderná-los:
Stream<Piloto> stream = pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.sorted(Comparator.comparing(Piloto::getNome));
No caso do stream o método de ordenação é o sorted(). Então qual a diferença entre ordernar uma lista com sort() e um stream com sorted() ? Se você respondeu que a diferença é que o stream não altera quem o gerou, você acertou ! No caso o stream não produz efeitos colaterais na lista de pilotos. Mas e se quisermos o resultado em uma lista ? Se você prestou atenção até aqui, já deve saber que precisaremos de um Collector, e que nosso código ficará assim:
List<Piloto> pilotosComMaisDe900Pontos = pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.sorted(Comparator.comparing(Piloto::getNome))
.collect(Collectors.toList());
Esse mesmo código no Java 7 seria mais ou menos assim:
List<Piloto> pilotosComMaisDe900Pontos = new ArrayList<>();
for (Piloto piloto:pilotos) {
if (piloto.getPontuacao() > 900) {
pilotosComMaisDe900Pontos.add(piloto);
}
}
Collections.sort(pilotosComMaisDe900Pontos, new Comparator<Piloto>() {
@Override
public int compare(Piloto o1, Piloto o2) {
return o1.getNome().compareTo(o2.getNome());
}
});
Veja que antes precisavamos de uma lista temporária, um laço e para esse mesmo filtro uma classe anônima para o Comparator até que finalmente teríamos a invocação para a ordenação. Lembre-se sempre, "Time is Money !"
Operações Lazy
Várias operações em streams são lazy !
Geralmente ao manipular um stream encadeamos diversas operações, a esse conjunto de operações damos o nome de pipeline ou pipeline de operacões.
O Java consegue tirar proveito dessa estrutura evitando executar operações o máximo possível, pois essas operações apenas serão de fato executadas somente quando realmente for necessário obter seu resultado final.
Vamos a um exemplo, considere a operação a seguir:
Stream<Piloto> stream = pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.sorted(Comparator.comparing(Piloto::getNome));
Note que os métodos filter() e sorted() devolvem um Stream, sendo assim no momento da invocação desses métodos eles nem filtram e nem ordenam, eles apenas devolvem novos streams em que essa informação (ordernar e filtrar) é marcada. Essas são chamadas de operações intermediárias.
Os streams retornados sabem que devem ser filtrados e ordenados no momento em que a última operação for invocada, nesse caso o collect() é essa última operação, ou operação terminal.
Agora você deve estar pensando em qual a vantagem em termos métodos lazy. Para entender vamos a um exemplo prático:
Imagina que precisamos encontrar um piloto com mais de 900 pontos, basta apenas um e qualquer um da lista serve, desde que tenha mais de 900 pontos (cumpra o critério do predicado).
Podemos pensar no seguinte código:
Piloto vencedor = pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.collect(Collectors.toList())
.get(0);
Tivemos muito trabalho para algo simples, veja que filtramos todos os pilotos, e criamos uma nova coleção com todos eles para pegar o primeiro da lista. Além disso, o que acontece no caso de não haver um piloto com mais de 900 pontos ? Teríamos uma exception ! Sendo assim, pensando nesse tipo de problea a API de streams possui o método findAny(), que devolve qualquer um dos elementos, considerando um predicado de um filtro.
Optional<Piloto> vencedor = pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.findAny();
Esse segundo código apresenta duas vantagens:
-
O método
findAny()devolve umOptional<Piloto>e com isso somos obrigados a fazer umget()ou usar os métodos de teste comoorElse()ouifPresent(). -
Como todo o trabalho foi lazy, o stream não foi inteiramente filtrado
O método findAny() é uma operação terminal e força a execução do pipiline de oprações, pertence a interface java.util.stream.Stream<T> e possui a seguinte assinatura:
Optional<T> findAny();
E observe uma de suas implementações:
@Override
public final Optional<P_OUT> findAny() {
return evaluate(FindOps.makeRef(false));
}
Veja que o retorno é um método chamado evaluate() que por sua vez recebe como parâmetro uma chamada para o método makeRef() da classe java.util.stream.FindOps, o evaluate() analisa as operações invocadas anteriormente e é inteligente o bastante para saber que não precisa filtrar todos os elementos da lista para pegar apenas um deles.
final <R> R evaluate(TerminalOp<E_OUT, R> terminalOp) {
assert getOutputShape() == terminalOp.inputShape();
if (linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
linkedOrConsumed = true;
return isParallel()
? terminalOp.evaluateParallel(this, sourceSpliterator(terminalOp.getOpFlags()))
: terminalOp.evaluateSequential(this, sourceSpliterator(terminalOp.getOpFlags()));
}
Sendo assim o método findAny() executa o filtro e assim que encontrar um piloto que cumpra o predicado (nesse caso ter mais de 900 pontos) retorna-lo e termina a filtragem. Se você é curioso o bastante viu que além do findAny() existe o findFirst(), então qual a diferença entre usar um ou outro ? A diferença é que o findFirst() utiliza os elementos na ordem percorrida pelo stream.
Para demonstrar isso vamos utilizar um método chamado peek() que nos permite inspecionar elementos do stream.
Inspecionando elementos do stream - peek()
O método `peek()`` na API de streams do Java 8 é uma operação intermediária que permite inspecionar cada elemento do stream à medida que eles são processados. Ele não modifica os elementos do stream, apenas os "espiões" (peeks) neles, permitindo que você execute ações adicionais sem afetar o fluxo da operação principal da stream.
Para demonstrar vamos alterar os getters para nome e pontuação da classe Piloto deixando eles assim:
package br.com.jorgerabellodev.lambda.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() {
// adicione esse print
System.out.println("getNome()");
return nome;
}
public int getPontuacao() {
// adicione esse print
System.out.println("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, imagina que queremos executar uma tarefa toda vez que processar um elemento, nesse exemplo vamos apenas imprimir o nome do primeiro piloto que for encontrado com mais de 900 pontos, para essa finalidade podemos utilizar o método peek()
pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.peek(System.out::println)
.findAny()
Considere o seguinte código:
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
public class Main {
public static void main(String[] args) {
BiFunction<String, Integer, Piloto> criadorDePilotos = Piloto::new;
List<Piloto> pilotos = new ArrayList<>();
pilotos.add(criadorDePilotos.apply("Prost", 122));
pilotos.add(criadorDePilotos.apply("Gasly", 180));
pilotos.add(criadorDePilotos.apply("Leclerc", 789));
pilotos.add(criadorDePilotos.apply("Senna", 1000));
pilotos.add(criadorDePilotos.apply("Albon", 562));
pilotos.add(criadorDePilotos.apply("Tsunoda", 967));
pilotos.add(criadorDePilotos.apply("Bottas", 609));
pilotos.add(criadorDePilotos.apply("Hamilton", 967));
pilotos.add(criadorDePilotos.apply("Verstappen", 890));
pilotos.stream()
.filter(piloto -> piloto.getPontuacao() > 900)
.peek(System.out::println)
.findAny();
}
}
Execute ele e observe a saída ! Veja que o método getPontuacao() foi invovado 4 vezes até encontrar um piloto com mais de 900 pontos.
O método peek(), diferente do forEach() não devolve void e não é uma operação terminal, ao invés disso ele devolve um novo stream e por tanto somos capazes de realizar outras operações lazy.
Outro exemplo seria o código que ordena pelo nome e imprime o primeiro ordenado:
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
public class Main {
public static void main(String[] args) {
BiFunction<String, Integer, Piloto> criadorDePilotos = Piloto::new;
List<Piloto> pilotos = new ArrayList<>();
pilotos.add(criadorDePilotos.apply("Prost", 122));
pilotos.add(criadorDePilotos.apply("Gasly", 180));
pilotos.add(criadorDePilotos.apply("Leclerc", 789));
pilotos.add(criadorDePilotos.apply("Senna", 1000));
pilotos.add(criadorDePilotos.apply("Albon", 562));
pilotos.add(criadorDePilotos.apply("Tsunoda", 967));
pilotos.add(criadorDePilotos.apply("Bottas", 609));
pilotos.add(criadorDePilotos.apply("Hamilton", 967));
pilotos.add(criadorDePilotos.apply("Verstappen", 890));
pilotos.stream()
.sorted(Comparator.comparing(Piloto::getNome))
.peek(System.out::println)
.findAny();
}
}
Note que nesse caso o método getNome() foi invocado para todos os elementos da lista.
Veja que o método peek() imprime todos os pilotos, mesmo se só quisermos fazer um findAny(), isso por que o método sorted() é o que se chama de statefull e operações desse tipo podem precisar processar todo o stream, mesmo que sua operação terminal não demande isso.
Operações de Redução
Uma operação de redução (reduction) é aquela que utiliza os elementos do stream para retornar um valor final, já usamos essas operações, um exemplo é o average().
double media = pilotos.stream()
.mapToInt(Piloto::getPontuacao)
.average()
.getAsDouble();
Há outros métodos de redução, como count(), max(), min() e sum().
O método sum(), assim como average() encontra-se apenas nos streams primitivos.
Os métodos min() e max() precisam de um Comparator como argumento.
Todos esses métodos exceto sum() e count() retornam um Optional.
Por exemplo se desejarmos somar todos os pontos de todos os pilotos
int total = pilotos.stream()
.mapToInt(Piloto::getPontuacao)
.sum();
Por baixo dos panos, essa soma é obtida a partir de uma operação de redução que funciona da seguinte forma:
Primeiro é criado um valor inicial para o somatório:
int valorInicial = 0;
IntBinaryOperator adicao = (a, b) -> a + b;
Na sequencia é executada uma lambda que faz a operação de adição (a, b) -> a + b; essa operação é o mesmo que escrever o seguinte método:
public int soma(int a, int b) {
return a + b;
}
A diferença é que como é uma lambda não retorna um int e sim um IntBinaryOperator que é uma interface funcional
@FunctionalInterface
public interface IntBinaryOperator {
int applyAsInt(int left, int right);
}
Daí com essas informações podemos fazer com que o stream processe a redução passo a passo:
int total = pilotos.stream()
.mapToInt(Piloto::getPontuacao)
.reduce(valorInicial, adicao);
Então em uma operação completa de redução teremos os seguintes componentes:
// um valor inicial
int valorInicial = 0;
// uma lambda ou alguma operação - nesse caso uma adição
IntBinaryOperator adicao = (a, b) -> a + b;
// a redução propriamente dita feita com o método reduce que recebe como parâmetro o valor inicial e a operação de redução (nesse caso adição)
int total = pilotos.stream()
.mapToInt(Piloto::getPontuacao)
.reduce(valorInicial, adicao);
// saída
System.out.println(total);
Esse código poderia ter sido escrito assim também:
int total = pilotos.stream()
.mapToInt(Piloto::getPontuacao)
.reduce(0, (a, b) -> a + b);
Apenas passando os valores para reduce()
E é assim que o método sum() funciona por baixo dos panos !
Qual a vantagem em utilizar reduce() dessa forma, no lugar de simplesmente utilizar sum() ? Nenhuma ! Porém é importante que você conheça esse método para poder executar determinadas operações em streams, como por exemplo multiplicar todos os pontos dos pilotos:
int totalDePontosMultiplicado = pilotos.stream()
.mapToInt(Piloto::getPontuacao)
.reduce(1, (a, b) -> a * b);
Repare que a lógica é a mesma da soma, temos um valor inicial que é 1, dois argumentos (a, b) e pedimos para retornar a multiplicação entre esses argumentos.