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