30 de março de 2019

Java 8 Streams: Pare de usar ‘for’ e simplifique seu código!

Streams do Java 8 deixam seu código mais legível e conciso, além de poderem aumentar sua produtividade ao lidar com listas e coleções. Aprenda agora e aumente a qualidade do seu código!

Loops tradicionais

Iterar sobre uma Collection antes do Java 5 era um código feio e complexo, o que sempre aumenta a chance de criar um bug inesperado. Lembremos:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 6, 3, 2, 1, 8, 5, 7, 4);
for (Iterator<Integer> numero = lista.iterator(); numero.hasNext();) {
    Integer integer = numero.next();
    System.out.print(integer);
}

Felizmente, a partir do Java 5 foi criada a nova sintaxe For-Each, que diminuiu muito a complexidade de iterar sobre coleções:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 6, 3, 2, 1, 8, 5, 7, 4);
for (Integer numero : lista) {
    System.out.print(numero);
}

Apesar da nova sintaxe ter facilitado muito a vida de desenvolvedores java, ela ainda não tirou proveito da programação funcional e é fortemente imperativa. No Java 8 isso mudou completamente.


Streams do Java 8

O Java 8 trouxe a criação de funções lambda. Se você ainda não está familiarizado com elas, pode conhecer mais clicando aqui. Com isso, foi possível criar uma nova forma de iterar sobre coleções, que é mais simples, concisa e ainda mais legível. Veja um exemplo simples, que faz o mesmo que os dois exemplos anteriores:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 6, 3, 2, 1, 8, 5, 7, 4);
lista.stream().forEach(System.out::print);

Essa sintaxe cria um stream de dados. Neste caso, os dados são os números. Para cada um deles é chamada a função System.out.print(...) passando o número como parâmetro. Aqui também estamos utilizando a sintaxe de method reference, que é essa forma de referenciar o método: System.out::print.


Operações intermediárias em Streams do Java 8

O método forEach que utilizamos acima é uma operação final, ou terminal, pois depois dela nada mais pode ser feito. Mas você pode fazer muito mais com Streams do Java 8. Vamos ver agora exemplos com operações intermediárias.

Os métodos skip, limit e distinct

Vejamos o exemplo abaixo que filtra alguns resultados antes de apresentá-los.

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
lista.stream()
    .skip(2) // ignora os dois primeiros números
    .limit(9) // limita a 9 números
    .distinct() // ignora números iguais
    .forEach(System.out::print);

A execução desse código produz a seguinte saída no console:

8742315

Nesse exemplo são feitas várias operações antes de executar o forEach:

  • O skip serve para ignorar os primeiros X itens. Aqui ignoramos o 1 e o 5.
  • O limit informa quantos objetos você quer tratar, a partir disso os próximos são ignorados. Aqui pegamos do 8 ao 5.
  • O distinct é igual ao DISTINCT do SQL: ele mantém apenas os resultados que são diferentes entre si. Para isso, ele utiliza o método equals dos objetos da lista. Aqui ignoramos a repetição dos números 2 e 8.

O método filter

As 3 funções que mostramos acima “filtram” o nosso stream, ou seja, ignoram alguns elementos. Mas e se for necessário um filtro mais personalizado? Para isso utiliza-se o método filter:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
lista.stream()
    .filter(e -> e % 2 == 0) // mantém apenas números pares
    .forEach(System.out::print); // imprime todos no console

O trecho de código acima produz a seguinte saída no console:

842284

Perceba que o método filter, nesse caso, está filtrando nosso stream para manter apenas números pares. Se você ainda não está familiarizado com as funções lambdas, a estrutura é a seguinte:

Java 8: estrutura da função lambda. Primeiro temos os parâmetros da função, depois o arrow token, depois do corpo da função.
Estrutura da função lambda passada para o método filter

O método map

Caso seja necessário fazer uma transformação no dado antes de passá-lo para o próximo método do stream, utiliza-se o método map. Apesar do nome, ele não tem relação com a interface Map. Veja abaixo.

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
lista.stream()
    .map(e -> e * 2) // multiplica cada item por 2
    .forEach(e -> System.out.print(e + " ")); // imprime todos no console

A saída no console é a seguinte:

2 10 16 14 8 4 6 4 2 16 10 14 8

Perceba que todos os números foram multiplicados por 2 antes de serem apresentados no console. Porém, é importante notar que os números da lista original não foram alterados. Ou seja, as transformações do método map afetam apenas os valores que serão passados para frente naquele stream. Isso é excelente, pois sempre que possível o ideal é trabalharmos com valores e instâncias imutáveis.


Operações finais em Streams do Java 8

Em todos os exemplos apresentados até agora o stream finaliza com um forEach. Essa é uma das operações finais disponíveis na API de Streams do Java 8. Ou seja, é um método que fecha o stream e coleta o resultado de tudo que foi feito. Veremos agora outras operações finais que geralmente são mais úteis do que o forEach. Afinal, você provavelmente irá querer armazenar o resultado das operações do stream, e utilizar esse resultado para alguma coisa. Vejamos algumas possibilidades.

Os métodos max, min, count

Se deseja pegar o maior ou o menor valor depois das operações realizadas no seu stream, os métodos max e min fazem exatamente isso. Vejamos o max:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
Optional<Integer> maiorNumero = lista.stream()
        .map(e -> e * 2) // multiplica cada item por 2
        .max(Comparator.naturalOrder()); // pega o maior item pela ordem natural
System.out.println(maiorNumero.get());

Ao utilizar o método max é necessário informar como o stream irá comparar seus objetos. Nesse caso, como Integer já implementa a interface Comparable e o método compareTo, ele já possui uma ordem natural que podemos utilizar, por isso usamos Comparator.naturalOrder(). Nesse exemplo o número 16 é impresso no console, pois é o maior número no stream depois de termos multiplicado todos por 2. Veja que, por ser uma operação final, é necessário armazenar o resultado em uma variável para utilizá-lo. O mesmo ocorre com o método min:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
Optional<Integer> menorNumero = lista.stream()
        .map(e -> e * 2) // multiplica cada item por 2
        .min(Comparator.naturalOrder()); // pega o menor item pela ordem natural
System.out.println(menorNumero.get());

Seguindo a mesma ideia do método max, esse exemplo produz a saída 2 no console.

Por fim, o método count retorna quantos elementos restam no stream:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
Long quantidade = lista.stream()
        .filter(e -> e % 2 == 0) // mantém apenas números pares
        .count(); // pega quantos itens restam no stream
System.out.println(quantidade);

Nesse exemplo a saída no console será 6, que é a quantidade de números pares no stream.

O método collect

E finalizando os exemplos das operações básicas, temos o método collect. Ele é a forma mais personalizável de coletar o resultado das operações do Stream. Por ter inúmeras formas de ser utilizado, falarei mais sobre ele em um artigo específico. Para saber assim que eu compartilhar, é só me seguir, os links estão no final deste artigo. Por enquanto vejamos alguns exemplos básicos com collect.

Se você quiser apenas coletar o resultado em uma nova lista, utilize um collector pré-implementado em Collectors.toList():

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
List<Integer> novaLista = lista.stream()
        .filter(e -> e % 2 == 0) // mantém apenas números pares
        .map(e -> e * 2) // multiplica cada item por 2
        .collect(Collectors.toList()); // coleta todos os itens em uma nova lista
System.out.println(novaLista);

Nesse exemplo filtramos apenas os números pares, multiplicamos todos por 2 e coletamos o resultado em uma nova lista. A saída no console é:

[16, 8, 4, 4, 16, 8]

Caso queira agrupar os itens da lista em um mapa, utilize o collector Collectors.groupingBy(...):

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
Map<Boolean, List<Integer>> mapa = lista.stream()
        .map(e -> e * 2) // multiplica cada item por 2
        .collect(Collectors.groupingBy(e -> e > 8)); // agrupa itens baseado no resultado da comparação
System.out.println(mapa);

Neste exemplo estamos multiplicando todos os itens do stream por 2, e depois agrupamos em números maiores que 8 e menores ou igual a 8. Veja como fica o mapa ao ser impresso no console:

{false=[2, 8, 4, 6, 4, 2, 8], true=[10, 16, 14, 16, 10, 14]}

E caso queira gerar uma única string a partir do stream, pode utilizar o collector Collectors.joining(...), que une várias strings em uma só:

List<Integer> lista = Arrays.asList(1, 5, 8, 7, 4, 2, 3, 2, 1, 8, 5, 7, 4);
String stringUnica = lista.stream()
        .map(String::valueOf) // transforma cada item em String
        .collect(Collectors.joining(";")); // junta todas as Strings em uma única separada por ';'
System.out.println(stringUnica);

Aqui transformamos cada número em uma string utilizando String.valueOf(…). Depois agrupamos todos em uma única string separando os elementos por ponto e vírgula. A saída no console é a seguinte string:

1;5;8;7;4;2;3;2;1;8;5;7;4

Parando de usar ‘for’ e ‘while’

Utilizando Streams do Java 8, raramente será necessário que você faça um loop explícito novamente, como for, while ou do...while. A API de streams utiliza loops implícitos, tira proveito da programação funcional, e deixa seu código muito mais legível e conciso. Além disso, em um próximo artigo mostrarei como você pode paralelizar a execução de um stream de forma muito simples!

Apesar de termos apresentado streams apenas a partir de listas, é possível criá-los de várias outras formas! Veja:

Parabéns! Agora você conhece melhor a API de Streams do Java 8! Você pode utilizar esse conhecimento para entregar no prazo soluções de alta qualidade.

Quer entender um pouco mais sobre funções lambda e sua estrutura? Veja esse artigo para entender facilmente a estrutura de funções lambda.

E você? Já teve que dar manutenção em algum código com streams? Lembra de situações em que você poderia ter utilizado streams? Deixe um comentário!

Share