domingo, 10 de maio de 2015

Java Funcional com Lambdas - Parte 3 (Stream)


Parte 1 - Introdução às expressões Lambdas
Parte 2 - As interfaces funcionais e as novidades da API Collections
Parte 3 - A classe Stream e conclusão 

A classe Stream

A API Collections foi adicionada ao Java desde a versão 1.2 da JDK e desde então muitas melhorias foram feitas. Para o Java 8, a maior novidade é que o tratamento de listas podem ser dar através de uma classe chamada Stream.
Localizada no novo pacote java.util.stream, Stream representa uma sequência de dados de um dado tipo. À essa sequência de dados, podemos aplicar diversas operações, tais como: filtrar, transformar os elementos para outros tipos, acumular ações sobre cada elemento, entre outros. O Stream é parametrizado através de genéricos, ou seja, eles têm um tipo e quando pegamos o stream de uma lista, os elementos do Stream terão também aquele tipo. Por outro lado, temos classes especializadas, tais como IntStream, LongStream e DoubleStream, que permitem que possamos ter funções mais específicas para esses tipos.
As funções aplicadas em um stream podem ser intermediárias ou terminais. As intermediárias permitem que outras funções ainda sejam aplicadas, sempre retornando um stream para processamento, e as terminais fecham os streams, não possibilitando aplicar novas funções. Quase todas fazem uso das interfaces funcionais descritas anteriormente. Destacamos as seguintes operações intermediárias de um Stream:
filter
Essa operação recebe uma implementação da interface Predicate, onde o tipo de parâmetro recebido é do tipo do Stream e a função deve retornar um boolean. Serve para filtrar elementos do stream de acordo com condições que o programador determina.
peek
A operação peek recebe uma implementação da interface Consumer onde o elemento do stream é passado para processamento e podemos utilizar o mesmo em algum processamento adicional.
map
Com map podemos transformar o stream de um tipo para outro. Para fazermos isso, a operação recebe uma implementação da interface do tipo Function, que deve receber o elemento do stream de um dado tipo e retornar um elemento para compor um novo stream do tipo desse retorno. Também temos as funções mapToInt, mapToLong e mapToDouble, onde o tipo de retorno é um Stream, como o nome da função diz, respectivamente dos tipos: int, long e double.

No caso de operações terminais, destacamos as seguintes:
count
Essa é uma operação terminal e ao chamar a mesma temos um retorno do tipo long que representa a quantidade de elementos que há no stream.
forEach
É uma função terminal semelhante à peek, onde temos a execução de um Consumer para cada elemento do stream.
toArray
Como o nome diz, essa operação retorna uma lista simples como os elementos do Stream. Note que o Array não irá conter elementos do tipo do stream, mas sim elementos do tipo Object.
reduce, max e min
A operação reduce utiliza uma interface do tipo BinaryOperator para reduzir os elementos do stream. As operações max e min são tipo particulares de operações que realizam a redução dos elementos dado um Comparator. O retorno é um objeto do tipo Optional. Há também as funções de redução para os Streams especializados, como sum, que soma todos os elementos de um IntStream, DoubleStream ou LongStream.
A interface Optional também foi adicionada à versão 8 da JDK e funciona como um contêiner para valores de um dado tipo. O objetivo é levar o programador a realizar um tipo de programação que evite a famigerada exceção NullPointerException. Com ela, podemos mais facilmente lidar com operações que podem resultar em nulo, evitando código “perigoso” que geralmente causam problemas inesperados em tempo de execução.
É ainda possível realizar transformações mais complexas dos elementos da Stream usando a função collect, que recebe um objeto do tipo java.util.stream.Collector, um interface que contém diversos métodos utilizatários para uso.
Para entermos melhor tudo que foi falado sobre stream até agora, vamos ver como ficaria o código da Listagem 7 quando usando lambdas e as novas características da API Collections na Listagem 11. Notem que a partir da lista de produtos, temos um stream e dele filtramos, aplicamos uma função usando o peek, ou seja, ainda temos o stream para usar, então, realizamos o mapeamento para Long, gerando um novo stream com que representa a quantidade de produtos no estoque, por fim invocamos a função terminal sum, aí temos a soma de todos os elementos que geramos após o mapeamento para long. Em seguidas, pegamos novamente o stream da lista, mas simplesmente filtramos os que são feitos antes de 2014 para então invocar a função processaAntigos. Por fim, reduzimos a lista a um Optional usando a função min e passando um Comparator da data de criação.


Listagem 11. Aplicando operações na lista de produtos usando a classe Steam
long somaProdutosMarcaSuper = produtos.stream()
     .filter(p -> p.qtdeEstoque > 5 && p.marca.equals("Super") && p.dataCriacao.get(ChronoField.YEAR) == 2014)
     .peek(p -> processaProdutoEmExcesso(p))
     .mapToLong(p -> p.qtdeEstoque).sum();
produtos.stream()
     .filter(p -> p.dataCriacao.get(ChronoField.YEAR) < 2000).forEach(p -> processaProdutoAntigo(p));
Optional maisAntigo = produtos.stream().min((p1, p2) -> p1.dataCriacao.compareTo(p2.dataCriacao));


Uma característica nova do Java 8 e que potencializa ainda mais o uso de Stream ao lidar com listas, é a possibilidade de usar referências a métodos. Através de :: (dois pontos), podemos eliminar código repetido ao tratar de listas com stream. Você pode passar a referência de qualquer método invés de ter que escrever a expressão Lambda inteira só para chamar o método. Ao fazer isso, a referência do método deve ser compatível com os parâmetros que se deseja receber e o retorno. Por exemplo, caso quisermos imprimir os elementos de um stream ao usar forEach, invés de fazer uma expressão para invocar System.out.println, podemos simplesmente passar uma referência para esse método: lista.forEach(System.out::println).
A Listagem 12 mostra a Listagem 11 reescrita usando referências a métodos. Vejam que ao chamar peek, invés de escrever toda a expressão Lambda, passamos a referência do nosso método estático e também usamos o método de acesso ao campo qtdeEstoque quando realizando o mapeamento para long.

Listagem 12. Aplicando operações na lista de produtos usando a classe Steam e referências de métodos
long somaProdutosMarcaSuper = produtos.stream()
     .filter(p -> p.qtdeEstoque > 5 && p.marca.equals("Super") && p.dataCriacao.get(ChronoField.YEAR) == 2014) 
     .peek(ProcessaProdutosRef::processaProdutoEmExcesso)
     .mapToLong(Produto::getQtdeEstoque).sum();
produtos.stream()
     .filter(p -> p.dataCriacao.get(ChronoField.YEAR) < 2000)
     .forEach(ProcessaProdutosRef::processaProdutoAntigo);

Conclusões

Nesse artigo foi feita a introdução das expressões Lambdas e como ela afeta a API base do Java 8. Essa novidade trouxe muitos benefícios para o desenvolvedor Java e renova a linguagem sem perder o que já temos estabelecido. Quem já usa JavaFX, trabalha com a API de Collections e outras APIs do Java, verá bastante uso das expressões Lambdas no seu dia a dia.
Para se especializar nesse novo mundo, a prática é necessário. Tentar reescrever código antigo usando expressões Lambdas e ler a documentação oficial irá acelerar ao leitor a familiarização com todas as novidades que vêm no versão 8 do Java. 
Para se aprofundar no tema veja os seguintes links:

Página da OpenJDK sobre as expressões Lambdas
http://openjdk.java.net/projects/lambda/
Site oficial da JSR 335 –Lambda Expressions for the JavaTM Programming Language
jcp.org/en/jsr/detail?id=335

Nenhum comentário:

Postar um comentário