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 aoDISTINCT
do SQL: ele mantém apenas os resultados que são diferentes entre si. Para isso, ele utiliza o métodoequals
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:
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:
- De um array utilizando
Arrays.stream(Object[])
; - De métodos estáticos em classes de stream, como
Stream.of(Object[])
,IntStream.range(int, int)
; - As linhas de um arquivo podem ser obtidas com
BufferedReader.lines()
; - Streams de arquivos podem ser obtidos através de métodos na classe
Files
; - Streams de números aleatórios podem ser obtidos em
Random.ints()
.
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!