Mesclando vários mapas usando Java 8 Streams

Publicado originalmente em http://preslav.me

Freqüentemente, nos deparamos com situações em que temos que mesclar várias instâncias de Map em uma única e garantir que as duplicatas de chave sejam tratadas adequadamente. Na maioria das linguagens de programação imperativas, incluindo Java, esse é um problema trivial. Com algumas variáveis ​​para armazenar o estado, alguns loops aninhados e várias instruções if-else, mesmo pessoas novas em Java poderiam programar uma solução em minutos. Ainda assim, é a melhor maneira de fazer isso? Embora garantido que funcione, tais soluções podem facilmente sair do controle e se tornar incompreensíveis em um ponto posterior do desenvolvimento.

O Java 8 trouxe consigo o conceito de streams e abriu a porta de possibilidades para a solução de tais problemas de maneira declarativa e funcional. Além disso, reduziu a necessidade de armazenar o estado intermediário nas variáveis, eliminando a possibilidade de algum outro código corromper esse estado em tempo de execução.

O problema

Suponha que temos dois mapas visitCounts1, visitCounts2 : Map<Long, Integer>onde os mapas representam os resultados de diferentes consultas de pesquisa. A chave de cada mapa é o ID de um determinado usuário, e o valor é o número de visitas do usuário a determinada parte do sistema. Queremos mesclar esses dois mapas usando fluxos, de forma que, onde a mesma chave (ID do usuário) apareça em ambos, queremos obter a soma (total) das visitas do usuário em todas as partes do sistema.

A solução

Primeiro, precisamos combinar todos os mapas em um fluxo unificado. Existem várias maneiras de fazer isso, mas a minha preferida é usar Stream.concat () e passar os conjuntos de entrada de todos os mapas como parâmetros:

Stream.concat(visitCounts1.entrySet().stream(), visitCounts2.entrySet().stream());

Aí vem a parte da coleta. Coletar em fluxos Java 8 é uma operação final. Ele pega um determinado fluxo, aplica todas as transformações (principalmente de mapa, filtro, etc.) e produz uma instância de um tipo de colecção Java comum: Lista, Conjunto, Mapa, etc. A maioria dos coletores comuns reside na classe de fábrica. Vou usar para meus propósitos.java.utils.stream.CollectorsCollectors.toMap()

A implementação padrão de Collectors.toMap () leva dois parâmetros lambda:

public static <T,K,U> Collector<T,?,MapCollector<K,U>> toMap(FunctionMapCollector<? super T,? extends K> keyMapper, FunctionFunctionMapCollector<? super T,? extends U> valueMapper);

Ao iterar no fluxo, os dois parâmetros lambda são chamados e transmitidos à entrada do fluxo atual como um parâmetro de entrada. O primeiro lambda deve extrair e retornar uma chave, enquanto o segundo lambda deve extrair e retornar um valor da mesma entrada. Este par de valores-chave serviria então para criar uma nova entrada no mapa final.

Combinando os dois primeiros pontos, nossa instância de mapa resultante ficaria assim agora:

Map<Long, Integer> totalVisitCounts = Stream.concat(visitCounts1.entrySet().stream(), visitCounts2.entrySet().stream())
.collect(Collectors.toMap(
entry
-> entry.getKey(), // The key
entry
-> entry.getValue() // The value
)
);

O que acontece aqui é bastante simples. O coletor usaria as chaves e valores dos mapas existentes para criar entradas no mapa resultante. Claro, tentar mesclar mapas com chaves duplicadas resultará em uma exceção.

Uma versão pouco conhecida do mesmo método aceita um terceiro parâmetro lambda, conhecido como “fusão”. Esta função lambda será chamada sempre que chaves duplicadas forem detectadas. Os dois valores possíveis são passados ​​como parâmetros e é deixado para a lógica na função decidir qual será o valor final. Este terceiro lambda torna a solução do nosso problema fácil e de uma maneira muito elegante:

Map<Long, Author> totalVisitCounts = Stream.concat(visitCounts1.entrySet().stream(), visitCounts2.entrySet().stream())
.collect(Collectors.toMap(
entry
-> entry.getKey(), // The key
entry
-> entry.getValue(), // The value
// The "merger"
(visitCounts1, visitCounts2) -> visitCounts1 + visitCounts2
)
);

Ou simplesmente, usando uma referência de método:

Map<Long, Author> totalVisitCounts = Stream.concat(visitCounts1.entrySet().stream(), visitCounts2.entrySet().stream())
.collect(Collectors.toMap(
entry
-> entry.getKey(), // The key
entry
-> entry.getValue(), // The value
// The "merger" as a method reference
Integer::sum
)
);