Home | Send Feedback

Do it yourself Multi Value Map with Java 8

Published: 16. September 2017  •  Updated: 25. October 2017  •  java

Java has a useful built-in collection library. But sometimes I need special collections that are not part of the standard library. One of this collection is the MultiValueMap. This collection is a map with a key and value, but the value is a collection instead of one single value. Like a one-to-many relation in a SQL database. For instance, an application could manage multiple subscribers to a topic in a simple messaging system with such a data structure.

Fortunately, there are many collection libraries out there that implement such a collection. Here are a few examples:

Apache Common Collections

org.apache.commons.collections4.SetValuedMap<String, Integer> aSubscribers = 
            new org.apache.commons.collections4.multimap.HashSetValuedHashMap<>();
aSubscribers.put("topic.a", 1);
aSubscribers.put("topic.a", 2);
aSubscribers.put("topic.a", 3);
aSubscribers.put("topic.a", 3);
aSubscribers.put("topic.b", 1);

aSubscribers.get("topic.a"); // [1, 2, 3]
aSubscribers.get("topic.b"); // [1]
aSubscribers.size(); // 4

Guava

com.google.common.collect.HashMultimap<String, Integer> gSubscribers = 
            com.google.common.collect.HashMultimap.create();
gSubscribers.put("topic.a", 1);
gSubscribers.put("topic.a", 2);
gSubscribers.put("topic.a", 3);
gSubscribers.put("topic.a", 3);
gSubscribers.put("topic.b", 1);
gSubscribers.get("topic.a"); // [1, 2, 3]
gSubscribers.get("topic.b"); // [1]
gSubscribers.size(); // 4

Eclipse Collections

org.eclipse.collections.api.multimap.set.MutableSetMultimap<String, Integer> ecSubscribers 
                 = org.eclipse.collections.impl.multimap.set.UnifiedSetMultimap.newMultimap();
ecSubscribers.put("topic.a", 1);
ecSubscribers.put("topic.a", 2);
ecSubscribers.put("topic.a", 3);
ecSubscribers.put("topic.a", 3);
ecSubscribers.put("topic.b", 1);
ecSubscribers.get("topic.a"); // [1, 2, 3]
ecSubscribers.get("topic.b"); // [1]
ecSubscribers.size(); // 4

All these libraries support different implementations of MultiValueMaps: synchronized, immutable, List as value, and much more.


Do it yourself

When you, for whatever reason, can't use one of these libraries, it's quite easy to build your own simple MultiValueMap. Thanks to lambdas and new methods introduced in Java 8, the code is very concise.

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class DiyMultiMap<K, V> {
    private final Map<K, Set<V>> multimap = new HashMap<>();

The computeIfAbsent() method takes two parameters. A key and a lambda. When the mapping with that key does not exist, computeIfAbsent() runs the lambda and creates a new mapping with the key and the return value of the lambda. In our case, an empty set. Because computeIfAbsent() always returns a set we can safely call add() afterwards.

    public void put(K key, V value) {
        this.multimap.computeIfAbsent(key, k -> new HashSet<>()).add(value);
    }

The getOrDefault() method returns either the value when it finds the mapping with the provided key or returns the second parameter, in this case an empty set.

    public Set<V> get(K key) {
        return this.multimap.getOrDefault(key, Collections.emptySet());
    }

Removing elements in a MultiValueMap is a bit tricky because we first have to remove the element from the set, then check if the set is empty, and if it is empty, remove the mapping from the map.
Thanks to the computeIfPresent() method and the ternary operator implementing, this becomes a one-liner. The method takes two arguments, the key, and a lambda. It runs the lambda when the mapping does not exist. The return value of the lambda becomes the new value unless it returns null; in that case, computeIfPresent() removes the mapping from the map.

    public void remove(K key, V value) {
        this.multimap.computeIfPresent(key,
                (k, set) -> set.remove(value) && set.isEmpty() ? null : set);
    }

Like the get() method the contains implementation calls getOrDefault() and can safely call contains() afterwards.

    public boolean contains(K key, V value) {
        return this.multimap.getOrDefault(key, Collections.emptySet()).contains(value);
    }

The size() method returns the total number of records in all value collections. For that, the method starts a stream iterating all value entries and maps them to an int by calling size(). The terminal operation sum() summarizes these totals into one number.

    public int size() {
        return this.multimap.values().stream().mapToInt(Set::size).sum();
    }

The next method returns all unique values. It streams over all value collections and flattens them into one set.

    public Set<V> values() {
        return (Set<V>) multimap.values().stream().flatMap(Set::stream);
    }
}
DiyMultiMap<String,Integer> diySubscribers = new DiyMultiMap<>();
diySubscribers.put("topic.a", 1);
diySubscribers.put("topic.a", 2);
diySubscribers.put("topic.a", 3);
diySubscribers.put("topic.a", 3);
diySubscribers.put("topic.b", 1);
diySubscribers.get("topic.a"); // [1, 2, 3]
diySubscribers.get("topic.b"); // [1]
diySubscribers.size(); // 4

This code is from the very interesting and informative Collections Refueled presentation by Stuart Marks. You find the recording of the presentation that Stuart Marks gave in April at Devoxx US on YouTube: https://www.youtube.com/watch?v=q6zF3vf114M

You can download the slides from his blog: https://stuartmarks.files.wordpress.com/2017/08/collectionsrefueled.pdf


Addendum

When you work with the Spring Framework you could use their MultiValueMap implementation from the util package. This is an implementation that uses a List and allows duplicates. Because the upcoming Spring 5 framework depends on Java 8, the developers rewrote the LinkedMultiValueMap and used some constructs we discussed in this blog.

org.springframework.util.MultiValueMap<String, Integer> springSubscribers 
     = new org.springframework.util.LinkedMultiValueMap<>();
springSubscribers.add("topic.a", 1);
springSubscribers.add("topic.a", 2);
springSubscribers.add("topic.a", 3);
springSubscribers.add("topic.a", 3);
springSubscribers.add("topic.b", 1);
System.out.println(springSubscribers.get("topic.a")); // [1, 2, 3, 3]
System.out.println(springSubscribers.get("topic.b")); // [1]
System.out.println(springSubscribers.size()); // 2