Entendendo a Stream API do Java 8

O Java 8 trouxe inúmeras funcionalidades, algumas delas como as expressões lambdas levaram a mudanças significantes na plataforma Java como um todo, não apenas na linguagem de programação, mas também em seu compilador e na própria JVM (Java Virtual Machine).

Uma das novidades que surgiram nessa nova versão é a Stream API que está intimamente relacionada com o Collection Framework do Java, como veremos mais a frente.

Um dos principais objetivos dessa nova API é facilitar o desenvolvimento reduzindo assim a quantidade de linhas de código necessárias para realizar uma operação. Para demonstrar um pouco dessas facilidades observe o código da Listagem 1.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9 , 0);

//Implementação tradicional
for(Integer n: list) {
   System.out.print(n);
}

//Implementação com expressões lambda e StreamAPI       
list.forEach(n-> System.out.print(n));
Listagem 1 – Imprimindo os elementos de um lista de inteiros

Nessa listagem, observamos duas situações: Na primeira realizamos um laço for sobre todos os elementos da lista da forma que estamos acostumados até o Java 7, na segunda utilizamos o novo método forEach() passando como parâmetro um expressão lambda que irá imprimir no console todos os elementos da lista. Nesse pequeno exemplo observamos como pode ser bem menos verboso programar utilizando essa nova API.

Uma das principais características da Stream API é que ela concentra em disponibilizar um trio de operações conhecido como Filter, Map e Reduce. Essas operações servem para filtrar dados em uma coleção e retorná-los de acordo com a necessidade da aplicação.

Vamos novamente a outro exemplo prática para entendermos melhor. Primeiramente vamos criar uma classe Pessoa como mostra a Listagem 2.

public class Pessoa {

   private String nome;
   private Integer idade;

   public Pessoa(String nome, Integer idade) {
	  this.nome = nome;
	  this.idade = idade;
   }
   // métodos getter e setter omitidos
}
Listagem 2 – Classe Pessoa.

Nosso objetivo agora é criar uma lista com algumas pessoas e filtrar todas as pessoas que comecem com a letra “A” e somar a idade de ambas.

Muito bem, olhando para esse problema parece ser simples resolvê-lo basta fazer um for na lista, executar um if para verificar se o nome da pessoa começa com a letra “A” e depois realizar o somatório da idade delas. Mas como fazer isso de maneira mais simples e com menos código? Vamos ver a solução na listagem 3.

List<Pessoa> listaPessoas = Arrays.asList(new Pessoa("Joao", 32),
  	                                      new Pessoa("Antonio", 20),	 
                                          new Pessoa("Maria", 18),
                                          new Pessoa("Angela", 30));
		
Stream<Pessoa> streamPessoas = listaPessoas.stream();

Integer somaIdade = streamPessoas.filter(p -> p.getNome().startsWith("A"))
			.mapToInt(p -> p.getIdade()).sum();
Listagem 3 – Somatório da idade das pessoas com inicial “A”.

Como observamos nessa listagem criamos uma lista de pessoas e em seguida convertemos essa lista para uma interface do tipo Stream executando o método stream(). A partir daí começamos a realizar as operações necessárias para trazer o resultado que queremos. Primeiramente executamos o método filter(), passando como parâmetro uma expressão lâmbda dizendo que queremos todas as pessoas que comecem com a letra “A” (p -> p.getNome().startsWith(“A”)).

Após isso, executamos o método mapToInt(), que realiza um mapeamentos por um determinado tipo de informação, que no caso é a idade. Por fim, chamamos o método sum() que irá somar as idades. Nesse instante completamos nossa operação e retornamos a somatória das idades.

O fato de chamarmos um método depois do outro (de forma encadeada) é outra característica da Stream API que possui interfaces fluentes que possibilitam esse tipo operação.

Em um primeiro momento pode parecer um pouco confuso essa abordagem, mas é apenas uma questão de trabalhar dessa forma e aos poucos nos tornamos mais confortáveis com essa sintaxe.

A Stream API fornece um recurso interessante quando temos que filtrar grandes volumes de dados em uma coleção. A fim de ganharmos em desempenho essa filtragem pode ser feita de forma paralela aproveitando o poder de processamento dos computadores. Para isso temos que obter uma instância da interface Stream executando o método parallelStream() como mostra a Listagem 4.

Stream<Pessoa> streamPessoas = listaPessoas.parallelStream();
Integer somaIdade = streamPessoas.filter(p -> p.getNome().startsWith("A"))
			.mapToInt(p -> p.getIdade()).sum();
Listagem4 – Filtrando dados de forma paralela

O código da listagem 4 difere do código da listagem 3 apenas na obtenção da instância da interface Stream ao executar o método parallelStream() na lista de pessoas. Com isso as operações de filtragem de dados serão realizadas de forma paralela.

A Stream API faz uso do framework de processamento paralelo Fork/Join presente na JDK desde o Java 7, tornando o controle de threads e demais recursos de paralelismo totalmente transparentes.

Até agora, foi mostrado como executar uma somatória de valores inteiros (idade) em uma lista de pessoas, mas e se quisermos saber qual é a maior e a menor idade das pessoas desta lista? Para resolver essa questão, vamos analisar o código da Listagem 5.

Stream<Pessoa> streamPessoas = listaPessoas.stream();
Integer somaIdade = streamPessoas.filter(p -> p.getNome().startsWith("A"))
                                .mapToInt(p -> p.getIdade()).sum();

Integer maiorIdadde = streamPessoas
                         .mapToInt(p-> p.getIdade()).max(). getAsInt();
Integer menorIdade = streamPessoas
                        .mapToInt(p -> p.getIdade()).min().getAsInt();
Listagem 5 – Filtrar a maior e menor idade das pessoas na lista

Ao executarmos esse código receberemos uma exceção, isso mesmo, uma exceção do tipo:
java.lang.IllegalStateException: stream has already been operated upon or closed.

Esse erro aconteceu por já termos utilizado a mesma instância da interface Stream para somarmos as idades e quisemos reaproveitá-la para descobrir a maior e menor idade das pessoas da lista. Mas por que essa exceção foi lançada?
Nesse ponto, entramos um pouco na maneira como o Stream gerencia suas operações as quais são de dois tipos: Intermediárias e Terminais.

As operações Intermediárias são aquelas que retornam um novo Stream para que novas operações intermediárias sejam realizadas de maneira fluente (encadeadas). Utilizamos esse tipo de operações quando executamos os métodos filter() e depois mapToInt() na Listagem 4, por exemplo.

As operações Terminais são operações que juntam os resultados de um Stream e retornam um valor ou um objeto. Depois de invocada uma operação terminal, o mesmo Stream não poderá ser alterado por outras operações intermediárias ou executar novas operações terminais. Exemplos de operações terminais são: forEach(), sum(), min(), max(), findFirst(), dentre outras. Uma maneira fácil de identificar uma operação terminal é observar o retorno dos métodos, uma operação terminal nunca retorna uma interface Stream.

Entretanto, existe uma maneira de recuperar vários dados de um Stream após serem filtrados e mapeados. Um exemplo de como podemos realizar essa operação encontra-se na Listagem 6.

Stream<Pessoa> streamPessoas = listaPessoas.stream();

IntSummaryStatistics intSummStat  = streamPessoas.filter(p -> p.getNome().startsWith("A"))
		         .mapToInt(p -> p.getIdade()).summaryStatistics();
		
System.out.println(intSummStat.getSum());
System.out.println(intSummStat.getMax());
System.out.println(intSummStat.getMin());
Listagem 6 – Recuperando várias informações de um Stream

Nessa listagem utilizamos a classe IntSummaryStatistics, com essa classe otimizamos o processo para obter os resultados de uma operação extraindo várias informações de um Stream, como podemos ver conseguimos com sucesso descobrir a somatória, a maior e a menor idade respectivamente.

A Stream API possui, três tipos especiais de interfaces além da própria interface Stream para trabalhar com os tipos primitivos int, double e long do Java, são elas: IntStream, DoubleStream e LongStream. A Listagem 7, mostra um exemplo de como podemos utilizar a interface IntStream , a mesma abordagem pode ser adotada para as outras duas interfaces.

IntStream intStream = listaPessoas.stream().mapToInt(p -> p.getIdade());
Double mediaIdades = intStream.average().getAsDouble();
Listagem 7 – Utilizando a interface IntStream

Nessa listagem ao executar o método mapToInt() retornamos uma instância de IntStream, outros métodos semelhantes como mapToLong() e mapToDouble() retornam respectivamente instâncias de LongStream e DoubleStream. Ainda na listagem 7, executamos o método average() que calcula a média de idade das pessoas da instância de Stream.

Um recurso interessante da Stream API é que podemos converter uma instância de Stream para uma Collection dos tipos List, Set e Map, como mostra a Listagem 8.

List<Pessoa> listPessoas = streamPessoas.filter(p -> p.getNome()
						  .startsWith("A"))
                          .collect(Collectors.toList());
		
Stream<Pessoa> streamPessoas2 = listaPessoas.stream();
Set<Pessoa> setPessoas = streamPessoas2.filter(p -> p.getNome()
					    .startsWith("A"))
                        .collect(Collectors.toSet());
Listagem 8 – Convertando um Stream para Collection

Nesta listagem, temos dois exemplos de como realizar uma conversão de uma instância de Stream para um List e para um Set. Em ambos os exemplos, filtramos os pessoas que começam com a letra “A” e depois executamos o método collect() juntamente com a classe Collectors que provê métodos para criação de coletores. Com esses coletores podemos converter um Stream para os tipos List, Set ou Map. Ao utilizar o método toList() ou toSet() da classe Collectors, convertemos um Stream para um List ou Set respectivamente.

Outro método da classe Collectors é groupingBy(), esse método agrupa os elementos de um Stream retornando uma instância da interface Map. Na Listagem 9, temos um exemplo de utilização desse método.

Map<Integer, List<Pessoa>> map = 
listaPessoas.stream().collect(Collectors.groupingBy(Pessoa::getIdade));
		
map.get(18).forEach(p -> System.out.println(p.getNome()));
Listagem 9 – Convertendo um Strem em um Map

Nesse exemplo, listagem 9, agrupamos as pessoas por idade passando para o método groupingBy(), a expressão lambda “Pessoa::getIdade” que acessa a idade de cada pessoa da lista. O resultado é um Map em que a chave é uma “idade” e o valor é uma lista de pessoas que possuem essa idade.

Na última linha da listagem 9, recuperamos todas as pessoas que tem 18 anos por meio do método get() de Map e em seguida fazemos um loop exibindo no console todos os nomes dessas pessoas utilizando o método forEach().

Finalmente, há um novo tipo no Java 8 que é a classe Optional. Essa classe é um repositório que guarda um objeto e disponibiliza métodos capazes de lidar com esse objeto seja ele nulo ou não. Para termos um referência para uma classe Optional é necessário invocar uma Operação Terminal em um Stream. As Operações Terminais que retornam um Optional são: reduce(), min(), max(), findFirst() e findAny(). Vamos a um exemplo de como podemos trabalhar com a classe Optional na Listagem 10.

Optional<Pessoa> optPessoa = listaPessoas.stream().filter(p -> p.getIdade() > 20).findFirst();
		
if (optPessoa.isPresent()) {
	Pessoa p = optPessoa.get();
	System.out.println(p.getIdade());
}
		
optPessoa.ifPresent(p -> System.out.println(p.getIdade()));
		
optPessoa.orElseThrow();
optPessoa.orElse(new Pessoa("Joao", 28));
Listagem 10 – Utilizando a classe Optional

Na listagem 10, filtramos todas as pessoas que possuem idade maior que 20 anos e retornamos o primeiro resultado dessa busca, por meio do método findFirst(), para o objeto Optional.

Logo abaixo não executamos uma verificação se optPessoa é igual a null, mas checamos se optPessoa existe, ou seja, isPresent(). Essa característica da classe Optional se deve ao fato de ela implementar o Design Pattern Null Object evitando assim de escrevermos aqueles vários if’s para garantir que um objeto não seja nulo.

Outros métodos que a classe Optional fornece são ifPresent() que realiza alguma ação passada como parâmetro caso exista um objeto, orElseThrow() que lança um exceção caso não exista objeto em Optional e orElse(), que executa uma ação caso não exista um objeto em Optional, em nosso exemplo criamos uma instância da classe Pessoa com o nome João e idade 28 anos.

Como podemos observar ao longo do post, com a Stream API realizamos várias operações com poucas linhas de código melhorando a legibilidade e a manutenção de nosso código. Essa API fornece vários recursos interessantes que podemos utilizar em nossos projetos com Java 8, por isso devemos praticar o uso desta API para tornarmos ainda mais fluentes em suas funcionalidades. Bons Estudos!

Por EDMAR BREGAGNOLI

Postado em: 23 de janeiro de 2015

Confira outros artigos do nosso blog

[Webinar] Profile de aplicações Java com Oracle Mission Control e Flight Recorder

24 de julho de 2017

Danival Calegari

Criando Mocks de serviços REST com SoapUI

27 de junho de 2017

Monise Costa

Three laws that enable agile software development

09 de março de 2017

Celso Gonçalves Junior

Medindo performance de uma API REST

21 de fevereiro de 2017

Monise Costa

Deixe seu comentário