[Pesquisar este blog]

domingo, 26 de novembro de 2017

Java 9::Melhorias na Stream API

As streams para coleções e as operações em massa foram uma grande adição do Java 8, pois sua utilização permite a realização de operações complexas com código simples e bastante direto.
 
 
A característica mais marcante desta API é possibilitar que cada elemento de uma coleção seja tratado sem a necessidade de construir explicitamente laços de repetição para tal processamento, ou seja, permitindo aplicar as operações desejadas nas coleções como um todo, o que se denominou de operações em massa (bulk operations) sobre as coleções.

O Java 9 traz alguns complementos úteis para esta API, como novas fontes de dados para as streams, além de algumas novas características.

Novas fontes de dados

Todas as coleções possuem um método stream() que permitem iniciar um stream pipeline, o qual poderá ser utilizado posteriormente numa sequência arbitrária de operações intermediárias, até que seja realizada uma última operação terminal. A stream que dá origem ao stream pipeline é conhecida como stream fonte (source stream).

Além da possibilidade de criar um stream fonte a partir das coleções, existia no Java 8 um conjunto limitado de possibilidades obtenção de stream fonte a partir de elementos externos às coleções, como por exemplo, java.io.BufferedReader.lines().

O Java 9 adiciona algumas novas fontes úteis a esse conjunto, por meio destes novos métodos:
  • java.util.Scanner.tokens()
  • java.util.regex.Matcher.results()
  • java.util.Optional.stream()

Em particular o método tokens() da classe Scanner é bem versátil, como mostra o exemplo que segue:
 

Novas características das Streams

Existem quatro novos métodos adicionados na interface java.util.stream.Stream<T>:
  • Stream<T> takeWhile(Predicate)
  • Stream<T> dropWhile(Predicate)
  • Stream<T> ofNullable(T)
  • Stream<T> iterate(T, Predicate< T>, UnaryOperator<T>)

Apenas recordando, a interface Stream<T> representa uma sequência de elementos (na verdade, uma cadeia de referências de objetos) sobre os quais uma ou mais operações podem ser executadas. As subinterfaces IntStream, LongStream e DoubleStream são especializações de Stream<T> voltadas para os tipos primitivos mais comuns, contendo algumas funcionalidades adicionais.

Stream<T> takeWhile(Predicate) e Stream<T> dropWhilePredicate)

Dois dos novos métodos são takeWhile(Predicate) e dropWhile(Predicate), semelhantes aos métodos existentes limit(long) e skip(long), mas que tomam um predicado ao invés de valores fixos. Um predicado é uma condição arbitrada pelo programador, ou seja, um critério de seleção, que retorna true ou false.

O método takeWhile(Predicate) considera os elementos iniciais de um stream que atendem o predicado/critério dado.

Nos fragmentos que seguem (que podem ser facilmente testados com uso do jshell), é declarada e inicializada uma coleção com 1000 valores sequenciais (0, 0.33, 0.66, 1.0, 1.33 ...).
 
 

 
Esta coleção pode ser exibida integralmente com:
 
 
 

O método limit(long) permite obter uma nova stream com, por exemplo, os n primeiros elementos da coleção, no caso os cinco primeiros:
 
 
 
O novo método takeWhile(Predicate), ao invés de tomar um número fixo dos primeiros elementos como limit(long), toma todos os primeiros elementos que atendem o critério dado (o predicado fornecido). Assim takeWhile(Predicate) vai selecionando os elementos presentes no início da stream enquanto tal predicado é satisfeito. No fragmento que segue, este método é usado para extrair os primeiros elementos da coleção que são menores do que 2.5. 
 

O método skip(long), por sua vez, produz uma nova stream pulando os n primeiros elementos da coleção (no fragmento que segue n = 990), produzindo resultados ilustrados a seguir.
 
 
 
 
 

Já o novo método dropWhile(Predicate), descarta todos os primeiros elementos que atendem o critério dado (o predicado fornecido). Assim dropWhile(Predicate) vai pulando os elementos presentes no início da stream enquanto seu predicado é satisfeito. No fragmento que segue, este método é usado para descartar os primeiros elementos da coleção que são menores do que 330.
 
 
 
 

 
Como os valores da coleção estão ordenados, o resultado de takeWhile é bastante previsível. Mas, quando o conteúdo da coleção não está ordenado, o uso de takeWhile requer cautela, pois o primeiro valor que não atende o predicado interrompe a seleção de elementos, o que pode ocorrer no primeiro elemento existente, produzindo como resultado um stream vazio.

Observe o resultado do fragmento abaixo que gera uma coleção com conteúdo aleatório.
 
 
 
 

 
 
 
 
Conforme o critério usado com takeWhile, o resultado pode ser um stream com conteúdo ou mesmo vazio, apesar de existirem valores na coleção que atendem o critério, mas não seus primeiros. O exemplo que segue mostra esta situação.
 
 
 
 
O mesmo pode acontecer no uso de dropWhile.

Considerando que a seleção e o descarte proporcionados, respectivamente, por takeWhile e dropWhile, são condicionais (i.e., baseado em um predicado), pode ser conveniente ordenar o stream de onde os elementos serão retirados. A ordenação de um stream pode ser facilmente obtida com o método sorted(), que retorna um stream classificado conforme o critério de ordenação natural de seu conteúdo.
 

Stream<T> ofNullable(T)

Outro elemento novo é o método estático ofNullable(T) da interface Stream<T>. Esta operação retorna uma stream do tipo T contendo um elemento ou nenhum, nos casos do argumento fornecido ser não nulo ou nulo. Este método é bastante útil para eliminar a verificação de elementos nulos antes da construção de uma stream. O fragmento que segue ilustra o uso deste novo método.
 
 
 
 
 
 

Stream<T> iterate(T, Predicate< T>, UnaryOperator<T>)

Também foi adicionada uma nova versão do método estático iterate, que toma três argumentos: o primeiro é um valor de inicialização (seed), o segundo é uma condição expressa por um predicado, e o último é uma função de incremento ou decremento. O objetivo deste método é simular um laço, tal como:
for (T index=seed; hasNext.test(index); index = next.apply(index)) {
...
}
 
Conforme os elementos fornecidos, pode obter tanto uma sequência vazia, como uma contendo um número finito de elemento, de maneira bem conveniente. Nos fragmentos que seguem, esta versão de iterate é usada para gerar duas sequências de inteiros, uma transformada em array com toArray() e outra coletada na forma de uma lista com collect.
 
 
 
 

Conclusões

As novas adições efetuadas na Stream API tornam esta biblioteca ainda mais conveniente e flexível de usar, lembrando que quão versátil são as streams e suas operações de filtragem, mapeamento e redução. É algo que vale a pena estudar!

Para saber mais