Last Updated: September 27, 2021
·
87.36K
· preslavrachev

Merging Multiple Maps Using Java 8 Streams

Originally published at http://preslav.me

Often, we're faced with situations, in which we have to merge multiple Map instances into a single one, and guarantee that key duplicates are handled properly. In most imperative programming languages, including Java, this is a trivial problem. With a few variables to store state, and a couple of nested loops, and several if-else statements, even people new to Java could program a solution within minutes. Yet, is this the best way to do it? Though guaranteed to work, such solutions could easily get out of hand and become incomprehensible at a later point of development.

Java 8 brought with itself the concept of streams, and opened the door of possibilities for solving such problems in a declarative functional manner. Moreover, it reduced the need of having to store intermediate state in variables, eliminating the possibility of some other code corrupting that state at runtime.

The Problem

Suppose, we have two maps visitCounts1, visitCounts2 : Map<Long, Integer> where the maps represent the results of different search queries. The key of each map is the ID of a certain user, and the value is the number of user visits to given part of the system. We want to merge those two maps using streams, such that where the same key (user ID) appears in both, we want to take the sum (total) of user visits across all parts of the system.

The Solution

First, we need to combine all maps into a unified Stream. There are multiple ways of doing this but my preferred one is using Stream.concat() and passing the entry sets of all maps as parameters:

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

Then, comes the collecting part. Collecting in Java 8 streams is a final operation. It takes a given stream, applies all transformations (mostly from map, filter, etc) and outputs an instance of a common Java colelction type: List, Set, Map, etc. Most common collectors reside in the java.utils.stream.Collectors factory class. I will use Collectors.toMap() for my purposes.

The default implementation of Collectors.toMap() takes two lambda parameters:

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

Upon iterating over the stream, both lambda parameters get called and passed the current stream entry as an input parameter. The first lambda is supposed to extract and return a key, whereas the second lambda is supposed to extract and return a value from the same entry. This key-value pair would then serve for creating a new entry in the final map.

Combining the first two points, our resulting Map instance would so far look like this:

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
    )
);

What happens here is rather straightforward. The collector would use the keys and values of the existing maps to create entries in the resulting map. Of course, trying to merge maps with duplicate keys will result in an exception.

A little known version of the same method accepts athird lambda parameter, known as the "merger". This lambda function will be called every time duplicate keys are detected. The two possible values are passed as parameters, and it is left to the logic in the function, to decide what the ultimate value will be. This third lambda makes solving our problem easy, and in a very elegant manner:

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
    )
);

Or simply, using a method reference:

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
    )
);