Skip to content

Latest commit

 

History

History
780 lines (710 loc) · 27 KB

File metadata and controls

780 lines (710 loc) · 27 KB

Utilizando Streams

Objetivo
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.

Criando um Stream

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.

  1. É possível criar um Stream a partir de um Array utilizando o método Arrays.stream().

    src/org/j6toj8/streams/usingstreams/Stream_ArraysStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ArraysStream.java[role=include]
    Saída no console
    A
    B
    C
  2. É 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.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_IntRangeStream.java[role=include]
    Saída no console
    0
    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.

  3. É possível criar um Stream a partir de uma lista.

    src/org/j6toj8/streams/usingstreams/Streams_ListStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ListStream.java[role=include]
    Saída no console
    A
    B
    C
  4. É possível criar um Stream a partir de elementos específicos utilizando o método Stream.of.

    src/org/j6toj8/streams/usingstreams/Streams_Of.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Of.java[role=include]
    Saída no console
    A
    B
    1
    2
    3.0
    4.0

    Nesse caso foi criado um Stream que contém: String, Character, Integer, Long, Float e Double.

Operações em Streams

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.

Operações intermediárias
  1. É possível ignorar elementos de um stream com a operaçao skip.

    src/org/j6toj8/streams/usingstreams/Stream_Skip.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Skip.java[role=include]
    Saída no console
    2
    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.

  2. É possível limitar a quantidade de elementos que serão processados utilizando a operação limit.

    src/org/j6toj8/streams/usingstreams/Streams_Limit.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Limit.java[role=include]
    Saída no console
    0
    1

    Nesse caso apenas os 2 primeiros elementos foram impressos no console, pois a operação limit limitou a quantidade de elementos a serem processados.

  3. É possível filtrar elementos do Stream utilizando a operação filter.

    src/org/j6toj8/streams/usingstreams/Streams_Filter.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Filter.java[role=include]
    Saída no console
    0
    2

    Nesse caso apenas os elementos pares foram impressos, pois a operação filter limitou àqueles que possuem resto da divisão por 2 igual a 0.

  4. É possível filtrar elementos repetidos do Stream utilizando a operação distinct.

    src/org/j6toj8/streams/usingstreams/Streams_Distinct.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Distinct.java[role=include]
    Saída no console
    A
    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 distinct utiliza os método equals e hashCode, 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 tipo String, que já possuem essa implementação por padrão.

  5. É possível aplicar uma transformação nos elementos do Stream utilizando a operação map.

    src/org/j6toj8/streams/usingstreams/Streams_Map.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Map.java[role=include]
    Saída no console
    0
    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.

  6. É possível ordenar os elementos de um Stream utilizando a operação sorted.

    src/org/j6toj8/streams/usingstreams/Streams_Sorted.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Sorted.java[role=include]
    Saída no console
    A
    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 interface Comparable, sendo apresentados em ordem alfabética. Também existe uma versão do método sort que recebe como argumento uma implementação de Comparator, caso deseje ordenar de outra forma.

  7. É possível observar os elementos que passam por um Stream utilizando a operação peek.

    src/org/j6toj8/streams/usingstreams/Streams_Peek.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Peek.java[role=include]
    Saída no console
    Peek: G
    ForEach: G
    Peek: T
    ForEach: T
    Peek: Y
    ForEach: Y
    Peek: A
    ForEach: A

    A operação peek funciona 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étodo peek e o forEach estão ambos realizando essa mesma ação. Porém, em aplicações reais, geralmente a operação final não será um forEach, de tal forma que fará sentido utilizar o peek.

  8. É possível transformar um Stream de vários Arrays em um único Stream contínuo utilizando o método flatMap.

    src/org/j6toj8/streams/usingstreams/Streams_FlatMap.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_FlatMap.java[role=include]
    Saída no console
    A
    B
    C
    D
    E
    F
    G
    H
    I

    Perceba que nesse caso existem 3 Arrays distintos. Então cria-se um Stream contendo 3 Arrays. O cenário comum seria um que cada elemento do Stream fosse um objeto do tipo Array. Porém, ao utilizar a operação flatMap, é criado um Stream para cada um desses Arrays, que são unidos e formam um único Stream contínuo.

Operações finais
  1. É possível executar uma ação final para cada elemento do Stream utilizando a operação forEach, conforme demonstrado nos exemplos anteriores.

  2. É possível recuperar o maior e o menor valor de um Stream utilizando as operações finais max e min. E também é possível recuperar a quantidade de elementos de um Stream utilizando a operação final count.

    src/org/j6toj8/streams/usingstreams/Streams_MaxMinCount.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_MaxMinCount.java[role=include]
    Saída no console
    Max: 9
    Min: 1
    Count: 8

    No caso das operações max e min, é necessário passar como argumento qual comparador será utilizado. Como os números possuem uma ordem natural, isto é, implementam a interface Comparable, é possível utilizar um comparador que usa essa ordem natural, que é o Comparator.naturalOrder(). Caso seja um tipo de objeto que não possui uma ordem natural, é necessário passar como argumento uma outra implementação de Comparator.

    As operações max e min retornam Optional pois, caso o Stream esteja vazio, será um Optional vazio. Desde o Java 8, com a adição da classe Optional, isso tem sido preferido ao invés de retornar null, pois facilita a programação funcional. A operação count não precisa de um Optional, pois mesmo com um Stream vazio irá retornar 0.

  3. É possível pegar o primeiro elemento do Stream utilizando a operação final findFirst, ou um elemento qualquer com findAny.

    src/org/j6toj8/streams/usingstreams/Streams_FindFirstAny.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_FindFirstAny.java[role=include]
    Saída no console
    First: 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 findAny pode trazer resultados diferentes.

    Assim como as operações max e min apresentadas anteriormente, findAny e findFirst retornam um Optional vazio caso o Stream esteja vazio.

  4. É possível verificar se os elementos do Stream atendem a alguma validação utilizando as operações finais allMatch, anyMatch e noneMatch.

    src/org/j6toj8/streams/usingstreams/Streams_Match.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Match.java[role=include]
    Saída no console
    anyMatch: 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.

  5. Não é possível chamar mais de uma operação final no mesmo Stream.

    src/org/j6toj8/streams/usingstreams/Streams_ReuseStream.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ReuseStream.java[role=include]
    Saída no console
    7
    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)
Pipeline
  1. É possível criar um pipeline com várias operações em um único Stream.

    src/org/j6toj8/streams/usingstreams/Streams_Pipeline.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Pipeline.java[role=include]
    Saída no console
    8
    12

    Para entender todas as operações realizadas nesse pipeline, é necessário entender passo a passo:

    1. Foi criado um Stream contendo todos os números de 0 a 9

    2. Foi aplicado um filtro mantendo apenas os números pares: 0, 2, 4, 6 e 8

    3. Foram ignorados os dois primeiros números, mantendo apenas: 4, 6 e 8

    4. Foi limitado o processamento aos dois primeiros números: 4 e 6

    5. Foi aplicada uma multiplicação por 2 em cada elemento, resultando em: 8 e 12

    6. Os dois elementos foram impressos no console.

  2. O Stream só será criado de fato depois que alguma operação for executada nele.

    src/org/j6toj8/streams/usingstreams/Streams_ChangeBackingList.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_ChangeBackingList.java[role=include]
    Saída no console
    1
    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 forEach foi invocado.

  3. É possível encadear a operação final do Stream utilizando expressões lambda na classe Optional.

    src/org/j6toj8/streams/usingstreams/Streams_Optional.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_Optional.java[role=include]

    Perceba que o método ifPresent é da classe Optional, mesmo que no segundo exemplo possa parecer que ele faz parte do Stream. Em outras palavras, a operação final é max, e ifPresent é uma chamada em Optional e não mais no Stream.

Execução sob-demanda (lazy)

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.

  1. Nada será feito se o Stream não contiver uma operação final.

    src/org/j6toj8/streams/usingstreams/Streams_LazyNoFinal.java
    link:../../../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.

  2. 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.java
    link:../../../src/org/j6toj8/streams/usingstreams/Streams_LazyMap.java[role=include]
    Saída no console
    Peek: 0
    ForEach: 0
    Peek: 1
    ForEach: 1
    Peek: 2
    ForEach: 2

    Perceba que, mesmo que a operação peek esteja antes da operação limit, ela não é executada para todos os elementos do Stream, apenas para aqueles que serão realmente utilizados.

Streams primitivos

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.

  1. É possível criar Streams de tipos primitivos com as classes: DoubleStream, IntStream e LongStream.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Primitives.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Primitives.java[role=include]
    Saída no console
     DoubleStream
    1.12.23.3
     IntStream
    123
    123
     LongStream
    123
    123
  2. É possível transformar um Stream comum em um Stream de primitivos utilizando as operações mapTo*.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_MapTo.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_MapTo.java[role=include]
    Saída no console
     Stream para IntStream
    1234
     Stream para LongStream
    1234
     Stream para DoubleStream
    1.02.03.04.0
  3. É possível gerar Streams infinitos com o método generate.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Generate.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Generate.java[role=include]
    Saída no console
     IntStream 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.

  4. É possível utilizar a operação rangeClosed ao invés de range, deixando o código mais legível.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_RangeClosed.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_RangeClosed.java[role=include]
    Saída no console
    123
    1234

    Perceba que na chamada utilizando range, o último número é exclusivo (não faz parte do Stream). No rangeClosed, tanto o primeiro quanto o último número são inclusivos (fazem parte do Stream).

  5. É possível gerar várias estatísticas de Streams utilizando a operação summaryStatistics.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_Statistics.java
    link:../../../src/org/j6toj8/streams/usingstreams/primitives/Streams_Statistics.java[role=include]
    Saída no console
    Quantidade: 10
    Maior: 9
    Menor: 0
    Soma: 45
    Média: 4.5

Reduce e Collectors

Reduce

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.

  1. É 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.java
    link:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_Reduce.java[role=include]
    Saída no console
    336

    Nesse 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.

  2. É possível criar uma operação de Reduce informando o valor de identidade.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_ReduceIdentity.java
    link:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_ReduceIdentity.java[role=include]
    Saída no console
    336

    Nesse 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.

  3. É 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.java
    link:../../../src/org/j6toj8/streams/usingstreams/reduce/Streams_ReduceCombiner.java[role=include]
    Saída no console
    336

    Nesse 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.

Collect

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.

  1. É possível utilizar um Collector que junta várias Strings.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorJoining.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorJoining.java[role=include]
    Saída no console
    ABC
  2. É possível utilizar um Collector para representar cada elemento como um número e calcular a média entre eles.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorAveragingInt.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorAveragingInt.java[role=include]
    Saída no console
    6.2
  3. É possível utilizar um Collector para armazenar os elementos de um Stream em uma nova coleção.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToCollect.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToCollect.java[role=include]
    Saída no console
    ArrayList: [1, 2, 3, 4]
    HashSet: [1, 2, 3, 4]
    LinkedList: [1, 2, 3, 4]
    TreeSet: [1, 2, 3, 4]
  4. É possível utilizar um Collector para armazenar os elementos de um Stream em um mapa.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToMap.java
    link:../../../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}
  5. Também é possível armazenar em um mapa para casos em que a chave for se repetir. O terceiro argumento do método toMap define a regra de mesclagem dos valores para chaves iguais.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorToMapDuplicateKey.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorToMapDuplicateKey.java[role=include]
    Saída no console
    {4=Luiz, 6=Amélia, 7=Rinaldo,Rodrigo,Roseany}
  6. É possível utilizar um Collector que cria um mapa agrupando valores que tem a mesma chave em uma lista.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingBy.java
    link:../../../src/org/j6toj8/streams/usingstreams/collect/Streams_CollectorGroupingBy.java[role=include]
    Saída no console
    {4=[Luiz], 6=[Amélia], 7=[Rinaldo, Rodrigo, Roseany]}
  7. Também é possível personalizar a maneira que o valores com chaves iguais serão combinados.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingByDownstream.java
    link:../../../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.

  8. Também é possível definir qual tipo de mapa será utilizado para agrupar.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorGroupingByMapFactory.java
    link:../../../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.

  9. É possível utilizar um Collector que particiona valores em True ou False a partir de um função do tipo Predicate.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorPartitioningBy.java
    link:../../../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.

  10. Também é possível personalizar como a combinação dos valores particionados será feita.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorPartitioningByDownstream.java
    link:../../../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 única String separados por vírgula.

  11. É possível adicionar uma camada a mais de transformação ao utilizar um Collector, utilizando o método mapping.

    src/org/j6toj8/streams/usingstreams/primitives/Streams_CollectorMapping.java
    link:../../../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.

Referências