Map vs. Multimap

Donald Raab
Javarevisited
Published in
5 min readApr 19, 2022

--

Discover a safer alternative to mapping keys to multiple values in Java.

Photo by Mulyadi on Unsplash

I need a Map with multiple values

At some point in your years of coding in Java, you have probably needed a Map<K, Collection<V>>. If you haven’t needed one yet, give it some time… you will. There are more convenient ways to manage the Collection part of the Map using methods available since Java 8. If you have never used computeIfAbsent before, it is a method well worth learning. The following is an example of creating a Map<String, List<String>> using computeIfAbsent.

@Test
public void mapOfList()
{
Map<String, List<String>> map = new HashMap<>();

Function<String, List<String>> ifAbsentFunction =
key -> new ArrayList<>();

map.computeIfAbsent("A", ifAbsentFunction).add("Apple");
map.get("A").add("Apricot");
map.computeIfAbsent("B", ifAbsentFunction).add("Banana");
map.computeIfAbsent("C", ifAbsentFunction).add("Cantaloupe");

Assertions.assertEquals(List.of("Apple", "Apricot"), map.get("A"));
Assertions.assertEquals(List.of("Banana"), map.get("B"));
Assertions.assertEquals(List.of("Cantaloupe"), map.get("C"));
Assertions.assertNull(map.get("D"));
}

The way computeIfAbsent works is simple. First, it looks in the map for a specified key (e.g. “A”). If it doesn’t find a value at the key, it evaluates the specified Function, and stores the result in the Map. It then returns either the value it found or the value it created and stored in the Map. So basically, computeIfAbsent is a get with a put where the value put into the Map is returned by the method. This method will guarantee a value always exists for any key looked up, but can be wasteful as a replacement for get, since it will always result in a put in the cases where you are looking up a key doesn’t exist.

Map has a null problem

One issue with using Map is that implementations like HashMap allow null keys, values and returns. If we ignore null keys for a second, the problem with null values and returns is that if you look up a key that does not exist in the Map, you will get a null back. You can try and protect your code against the possibility of checking the Map returns a null when calling get or by using a safer method like getOrDefault which could return an empty collection in the default case of a multi-valued Map. The real solution would be to create a Map type that knows that a missing key should result in an empty Collection being returned when calling get. This is what a Multimap does.

Don’t clown around with Maps w/ multi-values

If you need multi-valued Map support, then consider using a Multimap type from a library like Eclipse Collections, Google Guava or Apache Commons Collections. The method put on a Multimap will know that the values are multi-valued and should automatically call add on the value containers. The method get on a Multimap knows when a key is not contained in the Multimap, and that an empty Collection should be returned instead. The point is that a Multimap has more intimate knowledge about the value type it manages. A Map may be provided the value type via generics, but it does not know that the value type must be a type of Collection.

Using a Multimap in Eclipse Collections

The following code shows the equivalent solution to the Map code above using a Multimap type from Eclipse Collections.

@Test
public void mutableListMultimap()
{
MutableListMultimap<String, String> multimap =
Multimaps.mutable.list.empty();

multimap.put("A", "Apple");
multimap.put("A", "Apricot");
multimap.put("B", "Banana");
multimap.put("C", "Cantaloupe");

Assertions.assertEquals(List.of("Apple", "Apricot"), multimap.get("A"));
Assertions.assertEquals(List.of("Banana"), multimap.get("B"));
Assertions.assertEquals(List.of("Cantaloupe"), multimap.get("C"));
Assertions.assertEquals(List.of(), multimap.get("D"));
}

First, I create a specific type of Multimap, which in this case is a MutableListMultimap. Then I can simply call put with each key and value. The Multimap knows to create a backing Collection container (in this case a MutableList) for each new key. Finally, the call to multimap.get(“D”) returns an empty List. This List is not stored in the Multimap, so the Multimap will remain sparse and only contain the keys with actual values.

Multimap types in Eclipse Collections

There are several concrete Multimap types in Eclipse Collections. There are Readable, Mutable and Immutable interfaces. Multimap is the parent interface. MutableListMultimap is a leaf interface. The following are examples of the basic concrete Multimap types that can be created by using the Multimaps factory.

@Test
public void eclipseCollectionsMultimapTypes()
{
SerializableComparator<?> comparator = Comparators.naturalOrder();

// MutableMultimap Types
MutableBagMultimap<?, ?> mutableBagMultimap =
Multimaps.mutable.bag.empty();
MutableSetMultimap<?, ?> mutableSetMultimap =
Multimaps.mutable.set.empty();
MutableListMultimap<?, ?> mutableListMultimap =
Multimaps.mutable.list.empty();
MutableSortedSetMultimap<?, ?> mutableSortedSetMultimap =
Multimaps.mutable.sortedSet.with(comparator);
MutableSortedBagMultimap<?, ?> mutableSortedBagMultimap =
Multimaps.mutable.sortedBag.with(comparator);

// ImmutableMultimap Types
ImmutableBagMultimap<?, ?> immutableBagMultimap =
Multimaps.immutable.bag.empty();
ImmutableSetMultimap<?, ?> immutableSetMultimap =
Multimaps.immutable.set.empty();
ImmutableListMultimap<?, ?> immutableListMultimap =
Multimaps.immutable.list.empty();
ImmutableSortedSetMultimap<?, ?> immutableSortedSetMultimap =
Multimaps.immutable.sortedSet.with(comparator);
ImmutableSortedBagMultimap<?, ?> immutableSortedBagMultimap =
Multimaps.immutable.sortedBag.with(comparator);
}

With Multimap, there are specific types based on the value containers (e.g. List, Set, Bag). There are specializations and sometimes optimizations that may exist for those types.

GroupBy should return a Multimap

Eclipse Collections is the only Java library that I am aware of today that returns a Multimap from its groupBy methods on each of its basic Collection types. Each specific type like MutableList or MutableSet, will return the appropriate Multimap type in its groupBy method based on its type. MutableList returns a MutableListMultimap from its groupBy method, and MutableSet returns a MutableSetMultimap. The following is an example of using groupBy in Eclipse Collections.

@Test
public void groupBy()
{
MutableList<String> fruit =
Lists.mutable.with("Apple", "Apricot", "Banana", "Cantaloupe");

MutableListMultimap<String, String> multimap =
fruit.groupBy(each -> each.substring(0, 1));

Assertions.assertEquals(List.of("Apple", "Apricot"), multimap.get("A"));
Assertions.assertEquals(List.of("Banana"), multimap.get("B"));
Assertions.assertEquals(List.of("Cantaloupe"), multimap.get("C"));
Assertions.assertEquals(List.of(), multimap.get("D"));
}

For more examples of groupBy, you can refer to the following blog.

More information on Eclipse Collections Multimap

Nikhil Nanivadekar wrote a blog on Multimaps in Eclipse Collections a few years ago. He gives more in depth explanations on the implementations.

Summary

Do you need a Map with support for keys and multiple values? Consider using a Multimap from Eclipse Collections. If you’re already using other Collection types from Eclipse Collections, the groupBy methods will give you easy access to creating appropriate Multimap types.

I am a Project Lead and Committer for the Eclipse Collections OSS project at the Eclipse Foundation. Eclipse Collections is open for contributions. If you like the library, you can let us know by starring it on GitHub.

Other Java articles you may like

--

--

Donald Raab
Javarevisited

Java Champion. Creator of the Eclipse Collections OSS Java library (https://github.com/eclipse/eclipse-collections). Inspired by Smalltalk. Opinions are my own.