[Pesquisar este blog]

domingo, 20 de novembro de 2016

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

Neste terceiro artigo da série sobre Coleções e Streams, o assunto são as operações terminais. A API Streams, permite a manipulação do conteúdo das coleções de maneira muito flexível e elegante, possibilitando a execução de operações em massa sobre seus elementos, aproveitando a simplicidade das construções funcionais.

Revisão Rápida

A ideia de stream é representar uma sequência de elementos sobre os quais uma ou mais operações podem ser executadas. Um stream associado à uma coleção fornece uma sequência de seus elementos, onde cada uma figura uma única vez. Mas, diferentemente das coleções, cujo conteúdo é estável e pode ser reutilizado; ou dos iterators, que podem permitir a navegação entre os elementos das coleçõesos streams são 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.


A proposta da API de Streams é organizar um stream pipeline, como o ilustrado na figura anterior, iniciado  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).


O fragmento de código que segue mostra um stream pipeline com cinco estágios que, resumidamente, filtra os nomes da lista convertidos para maiúsculas com quatro caracteres que são terminados por "RO", contando a quantidade de ocorrências, mas mantendo a coleção lista inalterada:

// StreamFragm301.java
long c = lista.stream()
.map(s -> s.toUpperCase())
.filter(s -> s.length()==4)
.filter(s -> s.endsWith("RO"))
.count();

A coleção coleção de objetos String lista é acionada pelo método stream(), operação fonte que constitui o primeiro estágio onde é criado um stream que inicia o pipeline. No segundo estágio cada elemento é mapeado com map(Function<T,R>) para sua versão em maiúsculas. O dois estágios seguintes usam método filter(Predicate), uma operação intermediária (que toma um stream como entrada, resultando em outro), cuja expressões lambda fornecidas determinam 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.
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).

Neste artigo serão comentadas as operações de tipo terminal, lembrando que este material foi dividido em quatro artigos:


Operações Terminais

As operações terminais processam o conteúdo de um stream produzindo um objeto/valor único ou aplicando uma operação sobre todos os seus elementos. Em ambos os casos, o resultado não é um novo stream, o que impede a continuação do stream pipeline, encerrando-o (daí a terminologia de operação terminal).

As operações que produzem um resultado único, sob alguns aspectos são operações de redução dos elementos do stream no sentido de obter um valor especial que representa um aspecto específico do conjunto de elementos, como sua contagem, seu máximo, seu mínimo, uma ocorrência específica ou o resultado de sua redução.

Um outro tipo de operação terminal é aquela que aplica uma mesma operação a cada um dos elementos do stream, ou seja, efetuando uma operação em massa (ou bulk operation).

Também existem operações terminais que convertem um stream em um array, finalizando o pipeline.

Redução

A redução é uma operação terminal que processa, avalia ou agrega os elementos do stream resultando num valor ou objeto final que representa um aspecto específico do conjunto de elementos contidos no stream.

Redução
booleanallMatch (Predicate<T> p)
Retorna um resultado lógico que indica se todos os elementos do stream atendem o predicado p dado.
booleananyMatch (Predicate<T> p)
Retorna um resultado lógico que indica se ao menos um dos elementos do stream atendem o predicado p dado.
booleannoneMatch (Predicate<T> p)
Retorna um resultado lógico que indica se nenhum os elementos do stream atendem o predicado p dado.
longcount ( )
Conta o número de elementos contidos neste stream.
Optional<T>findAny ()
Retorna um objeto Optional<T> contendo um elemento qualquer do stream; ou um objeto Optional<T> vazio quando o stream não contém elementos.
Optional<T>findFirst ()
Retorna um objeto Optional<T> contendo o primeiro elemento do stream; ou um objeto Optional<T> vazio quando o stream não contém elementos.
Optional<T>max (Comparator<T> c)
Retorna um objeto Optional<T> contendo o maior elemento do stream segundo o comparador c fornecido; ou um Optional<T> vazio se o stream não contém elementos.
Optional<T>min (Comparator<T> c)
Retorna um objeto Optional<T> contendo o menor elemento do stream segundo o comparador c fornecido; ou um Optional<T> vazio se o stream não contém elementos.
Optional<T>reduce (BinaryOperator<T> acum)
Realiza a redução do stream por meio da função acumuladora-associativa acum, retornando um objeto Optional<T> com o resultado.
Treduce (T iden, BinaryOperator<T> acum)
Realiza a redução do stream, tomando iden como valor-identidade e acum como uma função acumuladora-associativa, retornando um objeto Optional<T> com o resultado.


No fragmento que segue, uma coleção de String, definida na variável lista, tem seu stream associado obtido e depois avaliado com os métodos allMatch(Predicate<T>)anyMatch(Predicate<T>)noneMatch(Predicate<T>), os quais respectivamente verificarão para um dado predicado se todos os elementos o atendem, se algum elemento o atende e se nenhum elemento o atende.
// StreamFrag301.java
List<String> lista = Arrays.asList("zEro", "Um", "DoIs", "TRes", "QuaTRo", "CiNcO", "SEIS", "setE", "oITo", "noVe", "DeZ");

// verifica se todas as strings são iniciadas com 'Q'
System.out.println("lista[ todos|inicial 'Q']? " +
lista.stream().
allMatch(s -> s.toUpperCase().startsWith("Q")));

// verifica se alguma string tem comprimento 3
System.out.println("lista[ algum|compr.==3]? " +
lista.stream().
anyMatch(s -> s.length()==3));

// verifica se nenhuma string é iniciada com 'x'
System.out.println("lista[nenhum|final 'x']? " +
lista.stream().
noneMatch(s -> s.toLowerCase().endsWith("x")));

O método findAny() retorna um elemento do stream (tipicamente o primeiro, mas sem garantia), enquanto findFirst() retorna o primeiro elemento do stream. Considerando as situações em que o stream está vazio, ao invés de retornar um valor null, que potencialmente poderia provocar o lançamento de NullPointerException, estes métodos sempre retornam um objeto de tipo Optional<T>. Um Optional<T> encapsula um outro objeto de qualquer tipo, ou mesmo um resultado null, simplificando o tratamento do resultado de operações que podem ou não retornar um valor.

O trecho que código abaixo mostra o uso de uma lista de nomes construída na classe Colecoes, onde se obtém um valor qualquer com findAny() e o primeiro valor com findFirst(). Observe as diferentes formas de uso do objeto Optional<String> retornado.
// StreamFragm302.java
List<String> lista = Colecoes.outrosNomes;

Optional<?> opt1 = null;
opt1 = lista.stream()
.findAny(); // obtém qualquer elemento
System.out.println(opt1);
lista.stream()
.findFirst() // obtém primeiro elemento
.ifPresent(opt2 -> System.out.println(opt2));

lista.stream()
.filter(s -> s.equals("Peter Jandl Junior"))
.findFirst() // obtém primeiro elemento
.ifPresent(opt2 -> System.out.println(opt2));

Os métodos max(Comparator<T>) e min(Comparator<T>) permitem obter o maior ou o menor elemento de um stream, retornando o resultado como um objeto Optional<T>, pois o stream pode estar vazio. O comparador fornecido deve atender a interface Comparator<T>. Segue um outro exemplo:
// StreamFrag303.java
List<String> lista = Colecoes.nomes;
Optional<?> opt = null;
opt = lista.stream()
.map(s -> s) // adição de operação intermediária dummy
.min(Comparator.comparing(s -> s.length()));
System.out.println(opt);

Colecoes.cores.stream()
.map(c -> c.getRed()) // mapeia cor RGB em seu valor Red
.max(Comparator.comparing(s -> s)) // comparador elementar
.ifPresent(v -> System.out.println(v));

Finalmente um exemplo do método de redução utilizado para obter a soma de uma série de valores (o componente azul) obtido a partir de uma coleção de objetos (uma lista de cores):
// StreamFrag304.java
long somaBlue = Colecoes.cores.stream()
.map(c -> c.getBlue()) // mapeia cor RGB em seu valor Blue
.reduce(0, (x,y) -> x + y); // redução (soma dos elementos)
System.out.println(somaBlue);

O uso de arrays ou listas de valores é ainda mais simples, pois não requer o mapeamento intermediário:
// StreamFrag304.java
Integer array[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int soma = 0; 

soma = Stream.of(array) // stream obtida de array
.reduce(0, (x,y) -> x + y); // redução (soma dos elementos)
System.out.println(soma);

List<Integer> lista = Arrays.asList(array); // lista obtida de array
soma = lista.stream() // stream obtida de lista
.reduce(0, Integer::sum); // redução (soma dos elementos)
System.out.println(soma);

Operação em Massa

São operações terminais onde uma operação específica é aplicada individualmente a todos os elementos presentes no stream, sem retorno de valor.

Operação em Massa
voidforEach (Consumer<T> c)
Aplica a função consumidora a cada elemento presente no stream, sem observar a ordem em que se encontram seus elementos (não-determinístico).
voidforEachOrdered (Consumer<T> c)
Aplica a função consumidora a cada elemento presente no stream, observando a ordem em que se encontram seus elementos (determinístico).

O método forEach() fará a aplicação a função consumidora dada em observar a ordem em que os elementos se encontram no stream. Como consequência não se pode prever como os elementos serão processados em conjunto (situação não-determinística), embora seja garantidos que a função Consumer<T> dada será aplicada a todos. O método forEachOrdered() observará a sequência de elementos presente no stream para aplicação da função, proporcionando um efeito determinístico, mas que não aproveitará da possível paralelização. Segue um fragmento ilustrando o :
// StreamFrag305.java
List<String> lista = Colecoes.nomes;
lista.stream().parallel() // stream paralela
.forEach(s->System.out.println(s));

System.out.println();

Colecoes.nomes.stream() // stream sequencial
.forEachOrdered(System.out::println);

Conversão

Permite transformar um stream em um array de elementos.

Conversão
Object[]toArray ()
Retorna um array contendo os elementos deste stream.

Um exemplo de uso da operação terminal de conversão é:
Object[] o = Colecoes.nomes.stream()
.filter(s -> s.startsWith("K"))
.toArray();

As operações terminais são um importante recursos no tratamento dos streams, pois permitem a realização de operações de redução, em massa ou sua conversão. No próximo post veremos aplicações interessantes da API Stream!


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

Para saber mais