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.