[Pesquisar este blog]

sábado, 5 de novembro de 2016

Coleções e Streams outra vez::Parte I

Uma das melhores novidades do Java 8 foi a adição da API Streams que trouxe novas ferramentas para a manipulação de conjuntos, listas e outras coleções. Sua característica mais marcante é possibilitar o tratamento de cada elemento de uma coleção sem a necessidade de construção explícita de laços de repetição para tal processamento, ou seja, a realização de operações em massa sobre as coleções. Esta série de quatro artigos pretende apresentar e detalhar esta importante API.
Hierarquia básica das interfaces do pacote java.util.stream.

Definição

A interface java.util.stream.BaseStream, que define sequencias de elementos que podem suportar operações agregadas executadas em modo sequencial ou paralelo, dá origem a quatro subinterfaces muito importantes do pacote java.util.stream.

A primeira é interface java.util.stream.Stream<T> representa uma sequência de elementos sobre os quais uma ou mais operações podem ser executadas. De fato, Stream<T> provê uma cadeia sequencial de referências de objetos, enquanto as demais subinterfaces (IntStream, LongStream e DoubleStream) são especializações voltadas para os tipos primitivos mais comuns, contendo algumas funcionalidades extras.

Para utilizar um stream é necessário organizar um stream pipeline. Tal pipeline se inicia com uma fonte (ou origem) dos dados; uma sequência de zero ou mais operações intermediárias; e uma operação terminal (ou final).



Nos fragmentos de código que seguem, o comentário inicial indica o nome arquivo-fonte Java onde estão contidos, permitindo que sejam testados com maior facilidade. Todos exemplos estão disponíveis no GitHub (projeto pjandl/stream_again).

 O fragmento de código que segue exibe um stream pipeline com três estágios:
// StreamFragm101.java
long cont = nomes.stream();
                 .filter(n -> n.startsWith("K"))
                 .count();

No primeiro, nomes é uma coleção de objetos String (por exemplo, List<String>), cujo método stream() é a  operação fonte que cria um stream e inicia o pipeline. O segundo estágio se constitui pelo método filter(Predicate), uma operação intermediária (que toma um stream como entrada, resultando em outro), cuja expressão lambda fornecida determina o predicado da filtragem, isto é, o critério de aceitação dos elementos do stream de entrada que serão enviados para o stream de saída. No último estágio temos a operação count(), que é terminal, pois produz um resultado long que não permite continuar encadeamento do stream pipeline.

Resumidamente, este pipeline filtra os nomes iniciados com "K", contando a quantidade de ocorrências. A coleção nomes permanece inalterada.

Um aspecto muito importante, que diferencia os streams das coleções, é que os streams são consumíveis, ou seja, só podem ser utilizados uma única vez por uma operação intermediária ou terminal. Caso sejam reutilizados será lançada a exceção IllegalStateException.

O encadeamento de métodos usado na construção de um pipeline poderia ser reescrito de maneira mais convencional como segue:
// StreamFragm101.java
Stream<String> stream_0 = nomes.stream();
Stream<String> stream_1 = stream_0.filter(n -> n.startsWith("K"));
long cont = stream_1.count();

O resultado proporcionado é o mesmo, mas a custa de código extra que não melhora a legibilidade das operações efetuadas. Por isso a construção dos pipelines é incentivada, evitando que variáveis intermediárias sejam incorretamente utilizadas.

Para podemos detalhar um pouco mais as operações de tipo fonte, intermediária e terminal; além da aplicação geral desta nova API, o material foi dividido em quatro artigos:

Operações Fonte

São aquelas capazes de iniciar um stream pipeline a partir de uma fonte, ou seja, que originam um stream que poderá ser utilizado posteriormente numa sequência de operações intermediárias e por uma última operação terminal.

As fontes são tipicamente coleções (subclasses de java.util.Collection), ou arrays; mas podem ser definidas como uma função geradora ou um canal de I/O. Observe que os mapas não são diretamente suportados com fontes.

Operações Fonte
static Stream<T> concat (Stream<T> a, Stream<T> b)
Retorna um stream cujos elementos são todos os existentes no stream a seguidos de todos os existentes no stream b.
static Stream<T> empty ( )
Retorna um stream sequencial vazio.
static Stream<T> generate (Supplier<T> s)
Retorna um stream sequencial infinito e não ordenado cujos elementos são gerados pela função provida.
static Stream<T> of (T... values)
Retorna um stream sequencial cujos elementos são aqueles fornecidos (na mesma ordem).
static Stream<T> of (T value)
Retorna um stream sequencial contendo um único elemento.

Dada uma coleção qualquer do tipo T, é possível tomá-la como fonte, ou seja, obter seu stream Stream<T> associado com:
// StreamFragm102.java
ArrayList<Integer> colecao = new ArrayList<Integer>();
// Adiciona conteúdo: números de 0 a 50, de 2 em 2
for (int i = 0; i < 50; i+=2) {
    colecao.add(i);
}
// Obtém stream
Stream<Integer> streamI = colecao.stream();

Também é possível obter um stream a partir de um array de qualquer tipo, tal como segue:
// StreamFragm102.java
// Obtém stream a partir de array de elementos do tipo Dimension
Dimension[] dimemsionArray = {
new Dimension(19, 64), new Dimension(68, 19), 
new Dimension(19, 31), new Dimension(95, 19),
new Dimension(13, 20), new Dimension(20, 16)
}; 

Stream<?> stream = Stream.of(dimemsionArray);

Outra possibilidade é concatenar dois streams existentes, na forma de um terceiro stream como abaixo:
// StreamFragm102.java
// Obtém stream de String a partir de outras streams de Strings

Stream<String> sAll = Stream.concat(nomes.stream(),
                                    outrosNomes.stream());

Finalmente é possível criar um stream, com infinitos elementos, com base numa função geradora, como o exemplo que segue:

// StreamFragm102.java
// Obtém stream a partir de gerador infinito
// de Double entre [0, 100)

Stream<Double> streamD = Stream.generate(() -> 100*Math.random());

A partir das operações fonte, obtemos streams que podem ser utilizadas por zero, uma ou mais operações intermediárias; e zero ou uma operação terminal. Mas isto fica para os próximos posts!



Este artigo faz parte de uma pequena série Coleções e Streams outra vez:



Para saber mais



Nenhum comentário: