Map vs. Multimap
Discover a safer alternative to mapping keys to multiple values in Java.
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.