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.
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.
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).
// StreamFragm101.java
long cont = nomes.stream();
.filter(n -> n.startsWith("K"))
.count();
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:
- Parte I:: Definições & Operações Fonte (este post)
- Parte II:: Operações Intermediárias
- Parte III:: Operações Terminais
- Parte IV:: Aplicações
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>();
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);
}
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
Outra possibilidade é concatenar dois streams existentes, na forma de um terceiro stream como abaixo:
// StreamFragm102.java
Finalmente é possível criar um stream, com infinitos elementos, com base numa função geradora, como o exemplo que segue:
// StreamFragm102.java
Este artigo faz parte de uma pequena série Coleções e Streams outra vez:
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());
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:
- Parte I::Definições & Operações Fonte (este post)
- Parte II::Operações Intermediárias
- Parte III::Operações Terminais
- Parte IV::Aplicações
Para saber mais
- Java - Guia do Programador. 3a Edição.Peter Jandl Junior.Novatec. 2015.
- The Collection Interface (The Java Tutorials).
- Lesson: Aggregate Operations (The Java Tutorials).
- Lambdas and Streams in Java SE 8.
Nenhum comentário:
Postar um comentário