A API Streams foi uma das melhores adições do Java 8. Por meio dela podemos manipular as coleções de maneira muito flexível, simples e elegante, realizando operações em massa sobre seus elementos, mas com uso de construções funcionais. Neste segundo artigo da série sobre Coleções e Streams, concentramos nossa atenção nas operações intermediárias.
Revisão Rápida
Um stream representa uma sequência de elementos sobre os quais uma ou mais operações podem ser executadas. Para utilizar um stream é necessário organizar um stream pipeline, como o ilustrado na figura anterior, que 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).
Além disso, os streams se diferenciam das coleções pelo fato de serem consumíveis, ou seja, só podem ser utilizados uma única vez por uma operação intermediária ou terminal. Sua reutilização provoca a exceção IllegalStateException.
Além disso, os streams se diferenciam das coleções pelo fato de serem consumíveis, ou seja, só podem ser utilizados uma única vez por uma operação intermediária ou terminal. Sua reutilização provoca a exceção IllegalStateException.
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() // obtém stream fonte
.filter(n -> n.startsWith("K"))// filtragem
.count(); // contagem
long cont = nomes.stream() // obtém stream fonte
.filter(n -> n.startsWith("K"))// filtragem
.count(); // contagem
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 terminal count() (que toma um stream como entrada e produz um tipo não encadeável) cujo resultado long não permite a continuação do encadeamento do stream pipeline.
Neste artigo serão detalhadas um pouco mais as operações de tipo intermediária, lembrando que este material foi dividido em quatro artigos:
- Parte I:: Definições & Operações Fonte
- Parte II:: Operações Intermediárias (este post)
- Parte III:: Operações Terminais
- Parte IV:: Aplicações
Operações Intermediárias
As operações intermediárias (que retornam um outro stream) possíveis são:
- filtragem (filtering),
- ordenação (sorting),
- mutação (mutation) e
- mapeamento (maping).
Filtragem
A filtragem é uma operação intermediária que seleciona elementos do stream baseado num critério (i.e., em um predicado). A filtragem não modifica o tipo de elemento contido no stream.Filtragem | |
Stream<T> | filter (Predicate<T> p) |
Retorna um stream consistindo dos elementos deste stream que atendem o predicado p dado. | |
Stream<T> | distinct ( ) |
Retorna um stream consistindo dos elementos distintos deste stream (diferentes segundo Object.equals(Object)). | |
Stream<T> | limit (long n) |
Retorna um stream consistindo dos n primeiros elementos deste stream. | |
Stream<T> | skip (long n) |
Retorna um stream consistindo elementos restantes deste stream após o descarte dos n primeiros elementos. |
Com uma coleção de Double definida na variável colecao é possível filtrar elementos que atendam um critério específico, separando uma amostra de tamanho determinado:
// StreamFrag201.java
Stream<Double> stream =
Stream<Double> stream =
colecao.stream() // obtém stream fonte
.filter(e -> e >= 5.0) // filtra valores >= 5
.limit(3); // ret. stream c/ (até) 3 elem.
Aqui a stream resultante proverá acesso para até três elementos maiores do que 5.0 dentre aqueles contidos na coleção. Uma construção equivalente, sem uso de streams, seria algo como:
List<Double> subcolecao = new ArrayList<>();
int tam = 3;
for(Double e:colecao) {
if (e>=5.0) {
subcolecao.add(e);
tam--;
if (tam==0) break;
}
}
Este código não é complexo, mas é maior e é menos direto que a construção em pipeline.
Outro exemplo é a obtenção da stream de caracteres de uma String, tratados como valores inteiros, que podem ser filtrados para obtenção dos elementos distintos. O método count() é uma operação terminal, de redução (que será abordada no próximo post) que conta a quantidade de elementos de um stream retornando um valor long.
// StreamFrag202.java
// obtém stream de char (valores int) de String
IntStream charList = string.chars();
// conta caracteres distintos
long count = charList.distinct().count();
System.out.println("Caracteres distintos: " + count);
Ordenação
É outra operação intermediária que possibilita classificar os elementos do stream segundo um critério. A ordenação não altera o tipo de elemento contido no stream.
Ordenação | |
Stream<T> | sorted ( ) |
Retorna um stream contendo os elementos deste stream classificados em ordem natural. | |
Stream<T> | sorted (Comparator |
Retorna um stream contendo os elementos deste stream classificados conforme o comparator fornecido. |
Com a mesma coleção de Double definida anteriormente, podemos criar um stream pipeline de cinco estágios, onde a ordenação é um deles:
// StreamFrag203.java
Stream<Double> stream =
Stream<Double> stream =
colecao.stream() // obtém stream fonte
.filter(e -> e < 6.0) // filtra valores < 6.0
.sorted() // ordena elementos
.skip(1); // descarta primeiro
.skip(1); // descarta primeiro
.limit(5); // ret. stream com (até) 5 elem.
Nesta situação a stream resultante proverá acesso aos cinco menores elementos, descartado o primeiro, da stream ordenada resultada da filtragem da stream original para valores menores que 6.0 existentes na coleção.
Mutação
Permite alterar os elementos do stream, conforme a função indicada, embora o tipo do elemento não seja alterado.
Mutação | |
Stream<T> | peek (Consumer<> action) |
Retorna um stream contendo os elementos deste stream, executando a ação provida em cada elemento. |
Um exemplo de uso da operação intermediária de mutação seria a obtenção de um stream com os quadrados do elementos contidos na coleção:
.peek(e -> Math.pow(e, 2));
Stream<Double> pipe2 = colecao.stream()
.peek(e -> Math.pow(e, 2))
.filter(e -> e > 10);
Várias operações intermediárias podem ser encadeadas, em qualquer ordem, assim podemos criar um stream com os quadrados dos elementos menores do que 1:
Stream<Double> pipe3 = colecao.stream()
.filter(e -> e < 1)
.peek(e -> Math.pow(e, 2));
Os streams resultantes (pipe1, pipe2 e pipe3) podem ser utilizados para outras operações intermediárias ou uma operação terminal.
Mapeamento
O mapeamento é uma operação intermediária mais sofisticada, que transforma os elementos do stream (considerandos como uma entrada do tipo T), com base em uma função de mapeamento (ou de transformação), e retorna um stream contendo elementos do tipo R, ou seja, esta é uma operação pode alterar o tipo dos elementos contidos no stream.
A função de mapeamento é responsável por tomar, individualmente, elementos do tipo T transformando-os em elementos do tipo R. É bastante comum que a função de mapeamento não altere os elementos de entrada, produzindo novos objetos com os dados ou características desejadas.
A função de mapeamento é responsável por tomar, individualmente, elementos do tipo T transformando-os em elementos do tipo R. É bastante comum que a função de mapeamento não altere os elementos de entrada, produzindo novos objetos com os dados ou características desejadas.
Mapeamento | |
Stream<R> | flatMap (Function<T, Stream<R>> mapper) |
Retorna um stream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento. | |
DoubleStream | flatMapToDouble (ToDoubleFunction<T> mapper) |
Retorna um DoubleStream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento. | |
IntStream | flatMapToInt (ToIntFunction<T> mapper) |
Retorna um IntStream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento. | |
LongStream | flatMapToLong (ToLongFunction<T> mapper) |
Retorna um LongStream consistindo dos resultados da substituição de cada elemento deste stream com o conteúdo do stream de mapeamento produzido pela aplicação da função de mapeamento a cada elemento. | |
Stream<R> | map (Function<T, R> mapper) |
Retorna um stream consistindo dos resultados da aplicação da função a cada elemento deste stream. | |
DoubleStream | mapToDouble (ToDoubleFunction<T> mapper) |
Retorna um stream especializado no tipo primitivo double consistindo dos resultados da aplicação da função a cada elemento deste stream. | |
IntStream | mapToInt (ToIntFunction<T> mapper) |
Retorna um stream especializado no tipo primitivo int consistindo dos resultados da aplicação da função a cada elemento deste stream. | |
LongStream | mapToLong (ToLongFunction<T> mapper) |
Retorna um stream especializado no tipo primitivo long consistindo dos resultados da aplicação da função a cada elemento deste stream. |
Considere o método estático que segue criado para ser um gerador de coleções de String. Embora retorne uma coleção de tipo indefinido, internamente este método instancia um ArrayList de String contendo uma centena de objetos String no formato CCDc, ou seja, dois caracteres maiúsculos, seguidos de um dígito e um caractere minúsculo.
// Generator.java
public class Generator {
public static int SIZE = 100;
public static Collection<String> newStringCollection() {
List<String> stringList = new ArrayList<>();
char[] charArray = new char[4];
for (int i = 0; i < SIZE; i++) {
charArray[0] = (char) (65 + (int) (Math.random() * 26));
charArray[1] = (char) (65 + (int) (Math.random() * 26));
charArray[2] = (char) (48 + (int) (Math.random() * 10));
charArray[3] = (char) (97 + (int) (Math.random() * 26));
stringList.add(new String(charArray));
}
return stringList;
}
}
A partir deste stream é possível a realização de várias operações intermediárias e terminais.
Uma nova coleção pode criada com:
// StreamFrag203.java
Collection<String> colecao = Generator.newStringCollection();
A quantidade de objetos desta coleção pode ser controlada por meio da definição de um outro valor para o campo estático SIZE.
Collection<String> colecao = Generator.newStringCollection();
A quantidade de objetos desta coleção pode ser controlada por meio da definição de um outro valor para o campo estático SIZE.
// StreamFrag203.java
Generator.SIZE = 20;
Collection<String> colecao = Generator.newStringCollection();
No trecho que segue, o stream obtido é filtrado para separar os elementos iniciados por 'J'. Uma operação de mapeamento conectada ao pipeline transforma os elementos, inserindo do sufixo "ava" após o 'J' inicial. Um último estágio para prover a ordenação do stream é incluso no pipeline.
// StreamFrag204.javaStream<String> stream = colecao.stream()
.filter(e -> e.charAt(0)=='J')
.map(e -> e.substring(0, 1) + "ava" + e.substring(1))
.sorted();
Assim o uso de operações intermediárias permite processar os streams, transformando-os conforme desejado e produzindo novos streams que podem ser utilizados por zero, uma ou mais operações intermediárias; e zero ou uma operação terminal. As operações terminais é assunto do próximo post desta série!
Este artigo faz parte de uma pequena série Coleções e Streams outra vez:
- Parte I::Definições & Operações Fonte
- Parte II::Operações Intermediárias (este post)
- Parte III::Operações Terminais
- Parte IV::Aplicações
Assim o uso de operações intermediárias permite processar os streams, transformando-os conforme desejado e produzindo novos streams que podem ser utilizados por zero, uma ou mais operações intermediárias; e zero ou uma operação terminal. As operações terminais é assunto do próximo post desta série!
Este artigo faz parte de uma pequena série Coleções e Streams outra vez:
- Parte I::Definições & Operações Fonte
- Parte II::Operações Intermediárias (este post)
- 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.
- 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.