Describe the Stream interface and pipelines; create a stream by using the Arrays.stream() and IntStream.range() methods; identify the lambda operations that are lazy. - Descrever a interface e pipelines de Stream; criar um stream utilizando os métodos Arrays.stream() e IntStream.range(); identificar quais operações lambda executam sob demanda (_lazy_).
Uma das maiores novidades do Java 8 são os Streams. Um Stream é basicamente um fluxo de dados. Os dados podem ser Strings, números, ou qualquer outro objeto. Esses dados passam por uma série de operações, e o conjunto dessas operações é chamado de pipeline. Essas operações são quase sempre representadas por expressões lambda, então é muito importante ter dominado todo o capítulo sobre lambda, pois todos aqueles conceitos serão utilizados agora para formar um Stream.
A partir dos exemplos a seguir, essa explicação ficará mais clara.
Geralmente, um Stream é criado a partir de um conjunto de dados, como uma lista ou outro tipo de coleção. O objetivo da certificação deixa explícito que é necessário conhecer os métodos Arrays.stream() e IntStream.range(). Mas, além dessas, serão apresentadas também algumas outras formas comuns de criar um Stream.
-
É possível criar um Stream a partir de um
Arrayutilizando o métodoArrays.stream().src/org/j6toj8/streams/usingstreams/Stream_ArraysStream.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_ArraysStream.java[role=include]
Saída no consoleA B C
-
É possível criar um Stream a partir de uma faixa de números utilizando o método
IntStream.range().src/org/j6toj8/streams/usingstreams/Stream_IntRangeStream.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_IntRangeStream.java[role=include]
Saída no console0 1 2 3
Perceba que o primeiro argumento (número 0) é inclusivo, enquanto o segundo argumento (número 4) é exclusivo. Por isso a saída no console apresenta apenas os números 0 a 3.
-
É possível criar um Stream a partir de uma lista.
src/org/j6toj8/streams/usingstreams/Streams_ListStream.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_ListStream.java[role=include]
Saída no consoleA B C
-
É possível criar um Stream a partir de elementos específicos utilizando o método
Stream.of.src/org/j6toj8/streams/usingstreams/Streams_Of.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Of.java[role=include]
Saída no consoleA B 1 2 3.0 4.0
Nesse caso foi criado um Stream que contém:
String,Character,Integer,Long,FloateDouble.
As operações feitas em um Stream irão formar seu pipeline. As operações que podem ser realizadas em um Stream são divididas em operações intermediárias e operações finais. O Stream pode conter inúmeras operações intermediárias, porém somente uma final. Nos exemplos anteriores a única operação utilizada foi o forEach, que é uma operação final. A seguir serão apresentadas outras operações.
-
É possível ignorar elementos de um stream com a operaçao
skip.src/org/j6toj8/streams/usingstreams/Stream_Skip.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Skip.java[role=include]
Saída no console2 3
Perceba que nesse caso os elementos 0 e 1 foram ignorados, pois são os dois primeiros elementos do Stream. Isso ocorreu pela existência da operação
skip. -
É possível limitar a quantidade de elementos que serão processados utilizando a operação
limit.src/org/j6toj8/streams/usingstreams/Streams_Limit.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Limit.java[role=include]
Saída no console0 1
Nesse caso apenas os 2 primeiros elementos foram impressos no console, pois a operação
limitlimitou a quantidade de elementos a serem processados. -
É possível filtrar elementos do
Streamutilizando a operaçãofilter.src/org/j6toj8/streams/usingstreams/Streams_Filter.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Filter.java[role=include]
Saída no console0 2
Nesse caso apenas os elementos pares foram impressos, pois a operação
filterlimitou àqueles que possuem resto da divisão por 2 igual a 0. -
É possível filtrar elementos repetidos do Stream utilizando a operação
distinct.src/org/j6toj8/streams/usingstreams/Streams_Distinct.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Distinct.java[role=include]
Saída no consoleA B C F
Perceba que nesse caso os elementos repetidos do stream (
"A"e"B") foram ignorados, sendo apresentados apenas uma vez.A operação
distinctutiliza os métodoequalsehashCode, então tenha certeza de que eles estão implementados corretamente caso esteja utilizando um tipo de objeto criado por você. No exemplo foram utilizados objetos do tipoString, que já possuem essa implementação por padrão. -
É possível aplicar uma transformação nos elementos do Stream utilizando a operação
map.src/org/j6toj8/streams/usingstreams/Streams_Map.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Map.java[role=include]
Saída no console0 2 4 6
Perceba que nesse caso os elementos sofreram uma transformação, que foi a multiplicação por 2, antes de serem impressos no console.
-
É possível ordenar os elementos de um Stream utilizando a operação
sorted.src/org/j6toj8/streams/usingstreams/Streams_Sorted.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Sorted.java[role=include]
Saída no consoleA A B B C F G T Y
Nesse caso todos os elementos são ordenados utilizando a ordem natural dos objetos
String, pois eles já implementam a interfaceComparable, sendo apresentados em ordem alfabética. Também existe uma versão do métodosortque recebe como argumento uma implementação deComparator, caso deseje ordenar de outra forma. -
É possível observar os elementos que passam por um Stream utilizando a operação
peek.src/org/j6toj8/streams/usingstreams/Streams_Peek.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Peek.java[role=include]
Saída no consolePeek: G ForEach: G Peek: T ForEach: T Peek: Y ForEach: Y Peek: A ForEach: A
A operação
peekfunciona apenas para observar o que está passando pelo Stream. Pode ser muito útil para realizar debug ou log. Nesse caso os elementos estão sendo impressos duas vezes no console, pois o métodopeeke oforEachestão ambos realizando essa mesma ação. Porém, em aplicações reais, geralmente a operação final não será umforEach, de tal forma que fará sentido utilizar opeek. -
É possível transformar um Stream de vários
Arraysem um único Stream contínuo utilizando o métodoflatMap.src/org/j6toj8/streams/usingstreams/Streams_FlatMap.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_FlatMap.java[role=include]
Saída no consoleA B C D E F G H I
Perceba que nesse caso existem 3
Arraysdistintos. Então cria-se um Stream contendo 3Arrays. O cenário comum seria um que cada elemento do Stream fosse um objeto do tipoArray. Porém, ao utilizar a operaçãoflatMap, é criado um Stream para cada um dessesArrays, que são unidos e formam um único Stream contínuo.
-
É possível executar uma ação final para cada elemento do Stream utilizando a operação
forEach, conforme demonstrado nos exemplos anteriores. -
É possível recuperar o maior e o menor valor de um Stream utilizando as operações finais
maxemin. E também é possível recuperar a quantidade de elementos de um Stream utilizando a operação finalcount.src/org/j6toj8/streams/usingstreams/Streams_MaxMinCount.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_MaxMinCount.java[role=include]
Saída no consoleMax: 9 Min: 1 Count: 8
No caso das operações
maxemin, é necessário passar como argumento qual comparador será utilizado. Como os números possuem uma ordem natural, isto é, implementam a interfaceComparable, é possível utilizar um comparador que usa essa ordem natural, que é oComparator.naturalOrder(). Caso seja um tipo de objeto que não possui uma ordem natural, é necessário passar como argumento uma outra implementação deComparator.As operações
maxeminretornamOptionalpois, caso o Stream esteja vazio, será umOptionalvazio. Desde o Java 8, com a adição da classeOptional, isso tem sido preferido ao invés de retornarnull, pois facilita a programação funcional. A operaçãocountnão precisa de umOptional, pois mesmo com um Stream vazio irá retornar0. -
É possível pegar o primeiro elemento do Stream utilizando a operação final
findFirst, ou um elemento qualquer comfindAny.src/org/j6toj8/streams/usingstreams/Streams_FindFirstAny.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_FindFirstAny.java[role=include]
Saída no consoleFirst: 7 Any: 7
Nesse caso, como o Stream é sequencial e não paralelo, os dois resultados são iguais. Em Streams paralelos, que serão apresentados em uma próxima seção, a operação
findAnypode trazer resultados diferentes.Assim como as operações
maxeminapresentadas anteriormente,findAnyefindFirstretornam umOptionalvazio caso o Stream esteja vazio. -
É possível verificar se os elementos do Stream atendem a alguma validação utilizando as operações finais
allMatch,anyMatchenoneMatch.src/org/j6toj8/streams/usingstreams/Streams_Match.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Match.java[role=include]
Saída no consoleanyMatch: true allMatch: false noneMatch: false
Perceba que na primeira operação é verificado se qualquer elemento é maior do que 5. Na segunda, se todos os elementos são maiores do que 5. E na terceira, se nenhum elemento é maior do que 5.
-
Não é possível chamar mais de uma operação final no mesmo Stream.
src/org/j6toj8/streams/usingstreams/Streams_ReuseStream.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_ReuseStream.java[role=include]
Saída no console7 2 1 Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279) at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580) at org.j6toj8.streams.usingstreams.Streams_ReuseStream.main(Streams_ReuseStream.java:11)
-
É possível criar um pipeline com várias operações em um único Stream.
src/org/j6toj8/streams/usingstreams/Streams_Pipeline.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Pipeline.java[role=include]
Saída no console8 12
Para entender todas as operações realizadas nesse pipeline, é necessário entender passo a passo:
-
Foi criado um Stream contendo todos os números de 0 a 9
-
Foi aplicado um filtro mantendo apenas os números pares: 0, 2, 4, 6 e 8
-
Foram ignorados os dois primeiros números, mantendo apenas: 4, 6 e 8
-
Foi limitado o processamento aos dois primeiros números: 4 e 6
-
Foi aplicada uma multiplicação por 2 em cada elemento, resultando em: 8 e 12
-
Os dois elementos foram impressos no console.
-
-
O Stream só será criado de fato depois que alguma operação for executada nele.
src/org/j6toj8/streams/usingstreams/Streams_ChangeBackingList.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_ChangeBackingList.java[role=include]
Saída no console1 2 3 4
Perceba que, mesmo que o Stream aparentemente tenha sido criado antes de adicionar o número 4 na lista, ele imprime esse número no console. Isso acontece porque o Stream só foi criado de fato quando alguma operação foi feita nele, ou seja, quando o
forEachfoi invocado. -
É possível encadear a operação final do Stream utilizando expressões lambda na classe
Optional.src/org/j6toj8/streams/usingstreams/Streams_Optional.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_Optional.java[role=include]
Perceba que o método
ifPresenté da classeOptional, mesmo que no segundo exemplo possa parecer que ele faz parte do Stream. Em outras palavras, a operação final émax, eifPresenté uma chamada emOptionale não mais no Stream.
As operações intermediárias de um Stream só são executadas quando necessário. Ou seja, mesmo que a operação esteja presente no pipeline, não é certeza de que ela será executada.
-
Nada será feito se o Stream não contiver uma operação final.
src/org/j6toj8/streams/usingstreams/Streams_LazyNoFinal.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_LazyNoFinal.java[role=include]
Nesse caso nada é impresso no console, pois nenhuma operação final foi aplicada no Stream. Ou seja, se não há nada consumindo o resultado desse Stream, o Java nem precisa executar o pipeline criado.
-
Outras operações intermediárias também não costumam ser executadas se não for necessário.
src/org/j6toj8/streams/usingstreams/Streams_LazyMap.javalink:../../../src/org/j6toj8/streams/usingstreams/Streams_LazyMap.java[role=include]
Saída no consolePeek: 0 ForEach: 0 Peek: 1 ForEach: 1 Peek: 2 ForEach: 2
Perceba que, mesmo que a operação
peekesteja antes da operaçãolimit, ela não é executada para todos os elementos doStream, apenas para aqueles que serão realmente utilizados.
Existem Streams específicos para alguns tipos primitivos como double, int e long. Eles possuem a vantagem de evitar o Boxing e Unboxing, fornecendo alguns métodos mais especializados como demonstrado a seguir.
-
É possível criar Streams de tipos primitivos com as classes:
DoubleStream,IntStreameLongStream.src/org/j6toj8/streams/usingstreams/primitives/Streams_Primitives.javalink:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Primitives.java[role=include]
Saída no consoleDoubleStream 1.12.23.3 IntStream 123 123 LongStream 123 123
-
É possível transformar um Stream comum em um Stream de primitivos utilizando as operações
mapTo*.src/org/j6toj8/streams/usingstreams/primitives/Streams_MapTo.javalink:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_MapTo.java[role=include]
Saída no consoleStream para IntStream 1234 Stream para LongStream 1234 Stream para DoubleStream 1.02.03.04.0
-
É possível gerar Streams infinitos com o método
generate.src/org/j6toj8/streams/usingstreams/primitives/Streams_Generate.javalink:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Generate.java[role=include]
Saída no consoleIntStream infinito de números aleatórios 2111846625 -1692075394 122693397 DoubleStream infinito de números aleatórios 0.913037010633669 0.23669861350384735 0.32655918031847697
Nesse caso os Streams são realmente infinitos. Só foram apresentados 3 números de cada pois existe a operação
limit, caso contrário a execução do programa também seria sem fim. -
É possível utilizar a operação
rangeClosedao invés derange, deixando o código mais legível.src/org/j6toj8/streams/usingstreams/primitives/Streams_RangeClosed.javalink:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_RangeClosed.java[role=include]
Saída no console123 1234
Perceba que na chamada utilizando
range, o último número é exclusivo (não faz parte do Stream). NorangeClosed, tanto o primeiro quanto o último número são inclusivos (fazem parte do Stream). -
É possível gerar várias estatísticas de Streams utilizando a operação
summaryStatistics.src/org/j6toj8/streams/usingstreams/primitives/Streams_Statistics.javalink:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Statistics.java[role=include]
Saída no consoleQuantidade: 10 Maior: 9 Menor: 0 Soma: 45 Média: 4.5
Reduce é uma das principais operações final que podem ser feitas em um Stream. Reduce é uma operação que transforma os vários valores do Stream em um único valor. Várias operações apresentadas anteriormente são um tipo de Reduce, como: max, min e summaryStatistics. Porém, nem sempre essas operações são suficientes, e por isso existem os métodos reduce. Eles permitem a implementação de operações personalizadas de Reduce.
-
É possível criar uma operação de Reduce personalizada com o método
reduce()que recebe 1 argumento.src/org/j6toj8/streams/usingstreams/primitives/Streams_Reduce.javalink:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_Reduce.java[role=include]
Saída no console336Nesse caso está sendo feito um Reduce onde o resultado da operação anterior é passado para a próxima execução. Ou seja, primeiro é feita a multiplicação de 7 * 2, que é 14. Então a função é chamada novamente passando como argumento o resultado anterior (14) e o próximo número do Stream (3). O resultado é 42. Então a função é chamada uma última vez passando o resultado anterior (42) e o próximo número do Stream (8), o que dá o resultado de 336.
-
É possível criar uma operação de Reduce informando o valor de identidade.
src/org/j6toj8/streams/usingstreams/primitives/Streams_ReduceIdentity.javalink:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_ReduceIdentity.java[role=include]
Saída no console336Nesse caso é possível informar o valor de identidade da função. O conceito de valor ou função de identidade são um pouco mais complexos, mas para a certificação apenas compreenda que ele representa um valor neutro. Ou seja, para a operação de multiplicação, o valor de identidade é 1, pois qualquer valor multiplicado por 1 resulta nele mesmo. Caso fosse uma operação de soma, o valor de identidade seria 0, pois qualquer valor somado a 0 resulta nele mesmo.
Além disso, se o Stream estiver vazio, o valor de identidade será retornado. Por isso, diferente do exemplo anterior, não é necessário retornar um
Optional. -
É possível criar uma operação de Reduce que pode ser executada em várias Threads e depois combinada em um único valor.
src/org/j6toj8/streams/usingstreams/primitives/Streams_ReduceCombiner.javalink:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_ReduceCombiner.java[role=include]
Saída no console336Nesse caso é passado um argumento adicional. Ele é a função de combinação. Essa função é utilizada quando o Stream é paralelo, ou seja, utiliza mais de uma thread. Ela pega o valor retornado por 2 ou mais threads e combina-os em um único valor. Em uma operação de multiplicação, a combinação também é uma multiplicação. Ou seja, caso a primeira thread multiplique 7 e 2, resultando em 14, e a segunda multiplique 3 e 8, resultando em 24, a função de combinação só precisa multiplicar 14 por 24 para chegar ao valor de 336. Sendo assim, a função de combinação só faz sentido em um Stream paralelo, que será apresentado no próximo capítulo.
A operação final collect também é um tipo de Reduce, porém é utilizada para objetos mutáveis. Ou seja, ao invés de utilizar a operação reduce com String, provavelmente seria mais eficiente utilizar a operação collect com a classe StringBuilder, para evitar a criação de vários objetos do tipo String. Como Java utiliza muitos objetos mutáveis, incluindo listas e mapas, geralmente a operação collect será mais eficiente do que a reduce.
Por serem muito comuns, existem vários Collectors já implementados no Java, disponíveis na classe Collectors.
-
É possível utilizar um
Collectorque junta váriasStrings.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorJoining.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorJoining.java[role=include]
Saída no consoleABC -
É possível utilizar um
Collectorpara representar cada elemento como um número e calcular a média entre eles.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorAveragingInt.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorAveragingInt.java[role=include]
Saída no console6.2 -
É possível utilizar um
Collectorpara armazenar os elementos de um Stream em uma nova coleção.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToCollect.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToCollect.java[role=include]
Saída no consoleArrayList: [1, 2, 3, 4] HashSet: [1, 2, 3, 4] LinkedList: [1, 2, 3, 4] TreeSet: [1, 2, 3, 4]
-
É possível utilizar um
Collectorpara armazenar os elementos de um Stream em um mapa.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToMap.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToMap.java[role=include]
Saída no console{Roseany=7, Amélia=6, Rodrigo=7, Rinaldo=7, Luiz=4} -
Também é possível armazenar em um mapa para casos em que a chave for se repetir. O terceiro argumento do método
toMapdefine a regra de mesclagem dos valores para chaves iguais.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToMapDuplicateKey.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToMapDuplicateKey.java[role=include]
Saída no console{4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany} -
É possível utilizar um
Collectorque cria um mapa agrupando valores que tem a mesma chave em uma lista.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingBy.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingBy.java[role=include]
Saída no console{4=[Luiz], 6=[Amélia], 7=[Rinaldo, Rodrigo, Roseany]} -
Também é possível personalizar a maneira que o valores com chaves iguais serão combinados.
src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingByDownstream.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingByDownstream.java[role=include]
Saída no console{4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany}Perceba que nesse caso os valores foram combinados utilizando outro Collector, que agrupou os nomes separando com vírgula.
-
Também é possível definir qual tipo de mapa será utilizado para agrupar.
src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingByMapFactory.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingByMapFactory.java[role=include]
Saída no console{4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany}Perceba que o resultado desse exemplo é idêntico ao anterior, porém foi passado um argumento a mais, que é o construtor do mapa que deveria ser utilizado.
-
É possível utilizar um
Collectorque particiona valores emTrueouFalsea partir de um função do tipoPredicate.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorPartitioningBy.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorPartitioningBy.java[role=include]
Saída no console{false=[Luiz, Amélia], true=[Rinaldo, Rodrigo, Roseany]}Perceba que nesse caso a regra de particionamento são os nomes que iniciam-se por
R. -
Também é possível personalizar como a combinação dos valores particionados será feita.
src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorPartitioningByDownstream.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorPartitioningByDownstream.java[role=include]
Saída no console{false=Luiz,Amélia, true=Rinaldo,Rodrigo,Roseany}Perceba que nesse caso os valores foram combinados utilizando um outro
Collector, que juntou os valores daquela mesma chave em uma únicaStringseparados por vírgula. -
É possível adicionar uma camada a mais de transformação ao utilizar um
Collector, utilizando o métodomapping.src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorMapping.javalink:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorMapping.java[role=include]
Saída no console{4=LUIZ, 6=AMÉLIA, 7=RINALDO,RODRIGO,ROSEANY}Esse tipo de código, apesar de complexo, pode aparecer no exame de certificação. É recomendado praticar esses exemplos com uma IDE para entender de fato seus comportamentos. Acesse os códigos de exemplo deste livro para facilitar seus estudos.
-
Using Streams
Boyarsky, Jeanne; Selikoff, Scott. OCP: Oracle Certified Professional Java SE 8 Programmer II Study Guide (p. 185). Wiley. Edição do Kindle.
-
Lesson: Aggregate Operations. The Java™ Tutorials.
-
Package java.util.stream. Java Plataform SE 8.
-
Interface Stream<T>. Java Plataform SE 8.