Search

Dark theme | Light theme

February 25, 2021

Java Joy: Merge Maps Using Stream API

In Java we can merge a key/value pair into a Map with the merge method. The first parameter is the key, the second the value and the third parameter of the merge method is a remapping function that is applied when the key is already present in the Map instance. The remapping function has the value of the key in the original Map and the new value. We can define in the function what the resulting value should be. If we return null the key is ignored.

If we want to merge multiple Map instances we can use the Stream API. We want to convert the Map instances to a stream of Map.Entry instances which we then turn into a new Map instance with the toMap method from the class Collectors. The toMap method also takes a remapping function when there is a duplicate key. The function defines what the new value is based on the two values of the duplicate key that was encountered. We can choose to simply ignore one of the values and return the other value. But we can also do some computations in this function, for example creating a new value using both values.

In the following example we use the Stream API to merge multiple Map instances into a new Map using a remapping function for duplicate keys:

package com.mrhaki.sample;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MapMerge {
    public static void main(String[] args) {
        Map<Character, Integer> first = Map.of('a', 2, 'b', 3, 'c', 4);
        Map<Character, Integer> second = Map.of('a', 10, 'c', 11);
        Map<Character, Integer> third = Map.of('a', 3, 'd', 100);

        // First we turn multiple maps into a stream of entries and
        // in the collect method we create a new map and define
        // a function to multiply the entry value when there is a 
        // duplicate entry key.
        Map<Character, Integer> result =
                Stream.of(first, second, third)
                      .flatMap(m -> m.entrySet().stream())
                      .collect(
                              Collectors.toMap(
                                      Map.Entry::getKey,
                                      Map.Entry::getValue,
                                      (value1, value2) -> value1 * value2));

        // The values for duplicate keys are multiplied in the resulting map.
        assert Map.of('a', 60, 'b', 3, 'c', 44, 'd', 100).equals(result);


        // In this sample the value is a Java class Characteristic.
        // The function to apply when a key is duplicate will create
        // a new Characteristic instance contains all values.
        // The resulting map will contain all concatenated characteristic values
        // for each key.
        var langauges =
                Stream.of(Map.of("Java", new Characteristic("jvm")),
                          Map.of("Clojure", new Characteristic("dynamic", "functional")),
                          Map.of("Groovy", new Characteristic("jvm", "dynamic")),
                          Map.of("Clojure", new Characteristic("jvm")),
                          Map.of("Groovy", new Characteristic("dynamic")),
                          Map.of("Java", new Characteristic("static")))
                      .flatMap(m -> m.entrySet().stream())
                      .collect(
                              Collectors.toMap(
                                      Map.Entry::getKey,
                                      Map.Entry::getValue,
                                      (c1, c2) -> c1.addCharateristics(c2.getValues())));

        assert new Characteristic("static", "jvm").equals(langauges.get("Java"));
        assert new Characteristic("dynamic", "functional", "jvm").equals(langauges.get("Clojure"));
        assert new Characteristic("dynamic", "jvm").equals(langauges.get("Groovy"));
    }

    /**
     * Supporting class to store language characteristics.
     */
    static class Characteristic {
        // Store unique characteristic value.
        private Set<String> values = new HashSet<>();

        Characteristic(String characteristic) {
            values.add(characteristic);
        }

        Characteristic(String... characteristics) {
            values.addAll(Arrays.asList(characteristics));
        }

        Characteristic addCharateristics(Set<String> characteristics) {
            values.addAll(characteristics);
            return this;
        }

        Set<String> getValues() {
            return values;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) { return true; }
            if (o == null || getClass() != o.getClass()) { return false; }
            final Characteristic that = (Characteristic) o;
            return Objects.equals(values, that.values);
        }

        @Override
        public int hashCode() {
            return Objects.hash(values);
        }
    }
}

Written with Java 15.