Multimap in Google Guava

Introduction

Google Guava is a library from Google that has a lot of utility collection classes. We already saw about Multiset in Google Guava.  In this post, we will look at a related class of Multiset – the Multimap in Google Guava.

Using Google Guava

You can include Google Guava into your project or application from Maven central. Refer to the Getting Google Guava section from one of my previous posts to know how to include the Google Guava library.

Multimap in Google Guava – What is it and when to use it?

A Multimap is a map but it can map multiple values against a key. When we have a scenario of having multiple values we have to write a considerable amount of code to maintain the list.
Example: We have a list of fruit names in a list. We want to construct a map of character to List of fruit names to map a character to the list of fruits that begin with that character. Normally we do it like:

List<String> fruits = Arrays.asList("apple", "banana", "orange", "avocado");
Map<Character, List<String>> map = new HashMap<>();
for (String fruit : fruits) {
    char firstChar = fruit.charAt(0);
    if (!map.containsKey(firstChar)) {
        map.put(firstChar, new ArrayList<>());
    }
    map.get(firstChar).add(fruit);
}
System.out.println(map); //{a=[apple, avocado], b=[banana], o=[orange]}

We can do better here. We can use computeIfAbsent method of the map to initialize the empty list for each new character seen.

for (String fruit : fruits) {
    char firstChar = fruit.charAt(0);
    map.computeIfAbsent(firstChar, c -> new ArrayList<>())
            .add(fruit);
}

The Multimap in Google Guava enables us to do exactly this with a simplified and powerful API. Let us dive into it.

The Multimap interface has two type parameters K and V. The value is not explicitly written as a collection. When we write Multimap<Integer, String> the value is a collection of String.

I will use a HashMultimap for explaining the Multimap APIs. When we move to the last part of this post, we will look at other implementing classes of a Multimap.

Adding to a multimap

A multimap interface provides two methods to add items to a multimap – the put and the putAll methods.

SetMultimap<String, String> multimap = HashMultimap.create();
multimap.put("1", "a");
multimap.put("2", "b");
multimap.put("2", "c");

System.out.println(multimap);

prints,

{1=[a], 2=[b, c]}

The key 1 has value a and the key 2 has values b and c. We can add multiple values for a key using putAll by passing an Iterable of values.

multimap.putAll("3", Arrays.asList("d", "e"));
System.out.println(multimap);//{1=[a], 2=[b, c], 3=[d, e]}

forEach method

We can print a multimap using the forEach method. It takes a BiConsumer to accept the key and value.

multimap.forEach((k, v) -> System.out.println(k + " = " + v));

prints,

1 = a
2 = b
2 = c
3 = d
3 = e

Multimap size

The size method of a multimap returns the number of key-value pairs in it (not the unique number of keys).

System.out.println(multimap.size()); //5

Query methods

Get

The get method gets the values mapped to a key as a Set.

System.out.println(multimap.get("1")); //[a]
System.out.println(multimap.get("2")); //[b, v]
System.out.println(multimap.get("22")); //[]

When queried for a non-existent key, it returns an empty set (and not a null).

Contains Key

The containsKey method takes a key and returns true if the multimap has at least one value mapped to that key.

System.out.println(multimap.containsKey("2")); //true
System.out.println(multimap.containsKey("22")); //false

Contains value

The containsValue method returns true if the multimap has the value passed (among all values across all the keys). In other words, it returns true if the multimap contains at least one key-value pair with this value.

System.out.println(multimap.containsValue("c")); //true
System.out.println(multimap.containsValue("f")); //false

Contains Entry

The containsEntry method returns true if the multimap has at least one key-value pair with the passed key and value.

System.out.println(multimap.containsEntry("2", "c")); //true
System.out.println(multimap.containsEntry("2", "a")); //false

Remove methods

For showing the remove methods, I will create a new Multimap from the original and remove elements from it to prevent the original from changing.

remove and removeAll

The remove method takes a key and a value and removes a single key-value pair. If there are multiple such key-value pairs, it removes one of them (which one is removed cannot be specified).
The removeAll takes a key and removes all the values mapped to that key. In addition to this, it returns the list of values removed as a Set.

SetMultimap copy = HashMultimap.create(multimap);
copy.remove("1", "a");
System.out.println(copy); //{2=[b, c], 3=[d, e]}

copy.removeAll("2");
System.out.println(copy); //{3=[d, e]}

Replace Values

This method (replaceValues) allows us to replace the values for a key. It returns the old/existed values for that key.

System.out.println(multimap.replaceValues("3", Arrays.asList("d1", "e1"))); //[d, e]
System.out.println(multimap); //{1=[a], 2=[b, c], 3=[e1, d1]}

To start off, the key 3 had values d and e. We replace it with d1 and e1. The next print statement confirms the current multimap contents.

View methods

The multimap offers several view methods (collection views over the original multimap).

keys

Returns the keys from each key-value pair from the multimap as a Multiset. It can thus have a key repeated multiple times (for those keys that have more than one value). Its size is same as the size of the multimap.

System.out.println(multimap.keys()); //[1, 2 x 2, 3 x 2]

The above toString representation is the implementation of a MultiSet. If a key is present once, it is shown as it is. If it is present more than once (more than one value mapped), it is shown as <the key> x <number of mappings>

keySet

It returns a view collection of all distinct keys in the multimap. The return type is thus a Set.

System.out.println(multimap.keySet()); //[1, 2, 3]

values

It returns a view of the collection of all the values from all key-value pairs in the multimap. The size of the returned collection is same as the multimap’s size.

System.out.println(multimap.values()); //[a, b, c, e1, d1]

entries

It returns a Set of multimap entries i.e., there will be one entry for each key-value pair.

System.out.println(multimap.entries()); //[1=a, 2=b, 2=c, 3=e1, 3=d1]

The asMap method

It returns a view of the multimap as a conventional Map.

System.out.println(multimap.asMap());//{1=[a], 2=[b, c], 3=[e1, d1]}

Multimaps class in Google Guava

In this section, we will look at the static utility methods in the Multimaps class.

Creating an unmodifiable multimap

We can make an existing multimap unmofiable by calling the unmodifiableMultimap method. There are specific methods for subinterfaces of Multimaps like SetMultimapListMultimap and SortedSetMultimap multimap.

If we have a SetMultimap, we can create an unmodifiable multimap like

SetMultimap<String, String> multimap = HashMultimap.create();
multimap.put("1", "a");
multimap.put("2", "b");
multimap.put("3", "c");


SetMultimap<String, String> unmodifiableMultimap = Multimaps.unmodifiableSetMultimap(multimap);

Attempting to add to the returned multimap will throw an UnsupportedOperationException as it is immutable.

Transform values and entries

Multimaps has a convenient method, transformValues, to convert the value(s) to some other value. As an example, using the multimap created above, let us convert the value by suffixing itself separated by a hyphen.

Multimap<String, String> transformedMultimap = Multimaps.transformValues(multimap, value -> value + "-" + value);
System.out.println(transformedMultimap);//{1=[a-a], 2=[b-b], 3=[c-c]}

So, the value a becomes a-a. 

The transformValues method takes the multimap as the first argument and a function as the second argument. The function will be invoked by passing each value in the multimap and the output of the function will be used as the new value. 

Note that this method returns a view of the passed multimap and hence the function is applied lazily. The function  thus can be applied many times when performing any query operation (like containsValue). If we need to work with the result often and want to avoid the function computation, we can copy the result into a new multimap.
 

If we want to do the value transformation also using the key of a key-value pair, we can use transformEntries. The second parameter is of type EntryTransformer which takes a key-value pair and returns a new value. Again, the returned multimap is just a view.

transformedMultimap = Multimaps.transformEntries(multimap, (k, v) -> k + "-" + v);
System.out.println(transformedMultimap);//{1=[1-a], 2=[2-b], 3=[3-c]}

Above, using the original multimap, we make the value a concatenation of the key and the value.

The index method

The index method takes an Iterable and a function and is used to construct a new multimap (not a view). The function is passed each of the values from the list and the result of the function will be the key of the multimap. and the element passed to the function will itself be the value.

Example: We have a list of fruits. We want to build a multimap of character to the list of fruits whose names begin with that character. We can do this as shown below:

List<String> list = Arrays.asList("apple", "banana", "orange", "avocado");
Multimap<Character, String> constructedMultimap = Multimaps.index(list, string -> string.charAt(0));
System.out.println(constructedMultimap);//{a=[apple, avocado], b=[banana], o=[orange]}

From the output shown in comments, the fruits apple and avacado gets mapped to the key a. Remember that we have to write 5-6 lines of code (at the start of this post) to do the same thing 🙂
Let us look at another example. We will create a mapping from the word length to the fruits with that length.

Multimap<Integer, String> lengthToFruitMultimap = Multimaps.index(list, String::length);
System.out.println(lengthToFruitMultimap);//{5=[apple], 6=[banana, orange], 7=[avocado]}

banana and orange are of length 6, apple is of length 5 and avacado is of length 7.

Filtering

We can create a view out of a multimap filtering entries that satisfy a predicate. There are three methods – filterKeys, filterValues and filterEntries. These enables us to specify the filter condition on the key, value or the entry (both key and value) respectively.
Let us look at an example. We will use the lengthToFruitMultimap built above. First, let us filter and get only the entries of odd length.

Multimap<Integer, String> oddLengthFruits = Multimaps.filterKeys(lengthToFruitMultimap, k -> k % 2 == 1);
System.out.println(oddLengthFruits); //{5=[apple], 7=[avocado]}

Now, let us get only the fruits whose name begins with a vowel.

private static Predicate<String> startsWithVowel() {
    List<String> vowels = Arrays.asList("a", "e", "i", "o", "u");
    return str -> vowels.stream().anyMatch(vowelCharacter -> str.startsWith(vowelCharacter));
}
Multimap<Integer, String> fruitsStartingWithAVowelMultimap =
        Multimaps.filterValues(lengthToFruitMultimap, startsWithVowel());
System.out.println(fruitsStartingWithAVowelMultimap);//{5=[apple], 6=[orange], 7=[avocado]}

Note that there are two fruits mapped to key 6 (orange and banana). But only one (orange) begins with a vowel.
Let us combine these two and filter the entries whose key is of odd length and the value begins with a vowel.

Multimap<Integer, String> resultMultimap =
        Multimaps.filterEntries(lengthToFruitMultimap, (entry) -> entry.getKey() % 2 == 1
                && startsWithVowel().test(entry.getValue()));
System.out.println(resultMultimap);//{5=[apple], 7=[avocado]}

Multimap Implementing classes

The Multimap interface recommends to not use it directly. Instead, use one of the sub-interfaces (like SetMultimap or ListMultimap). Now, we will look at a few classes that implement the Multimap.

HashMultimap

It implements Multimap using Hash tables (HashMap). Hence, it does not guarantee ordering among keys or values mapped to a key. It also does not allow duplicate values for a key (duplicate key-value pair). In other words, we can add a value only once for a key.

SetMultimap<String, String> hashMultimap = HashMultimap.create();
hashMultimap.put("2", "d");
hashMultimap.put("1", "a");
hashMultimap.put("1", "c");
hashMultimap.put("1", "b");
hashMultimap.put("1", "b");
System.out.println(hashMultimap);

The above code outputs

{1=[a, b, c], 2=[d]}

Though we add the key 2 before 1, since it is using a HashMap, the output does not guarantee ordering. Here, we get the entry for key 1 before key 2. Adding the value b twice does not add it twice as it uses a HashSet for the values.

 

LinkedHashMultimap

This implementation of multimap is based out of LinkedHashMap for the key and LinkedHashSet for the values. It thus preserves the insertion order, but does not allow duplicate key-value pairs.

SetMultimap<String, String> linkedHashMultimap = LinkedHashMultimap.create();
linkedHashMultimap.put("2", "d");
linkedHashMultimap.put("1", "a");
linkedHashMultimap.put("1", "c");
linkedHashMultimap.put("1", "b");
linkedHashMultimap.put("1", "b");
System.out.println(linkedHashMultimap);//{2=[d], 1=[a, c, b]}

We can see the insertion order being preserved for both keys (2 before 1) and values (a, c, b).

ArrayListMultimap

It uses HashMap for the keys and an ArrayList for the values. Hence, it does not maintain insertion order for keys, but since it is using an ArrayList for the values it guarantees insertion order and can have duplicates.

ListMultimap<String, String> arrayListMultimap = ArrayListMultimap.create();
arrayListMultimap.put("2", "d");
arrayListMultimap.put("1", "a");
arrayListMultimap.put("1", "c");
arrayListMultimap.put("1", "b");
arrayListMultimap.put("1", "b");
System.out.println(arrayListMultimap);//{1=[a, c, b, b], 2=[d]}

LinkedListMultimap

Uses a LinkedHashMap for the key and a LinkedList for the values. It stores data (key and value) in the order of insertion and can have duplicates.

ListMultimap<String, String> linkedListMultimap = LinkedListMultimap.create();
linkedListMultimap.put("2", "d");
linkedListMultimap.put("1", "a");
linkedListMultimap.put("1", "c");
linkedListMultimap.put("1", "b");
linkedListMultimap.put("1", "b");
System.out.println(linkedListMultimap);//{2=[d], 1=[a, c, b, b]}

TreeMultimap

Uses a TreeMap and a TreeSet for keys and values respectively. Hence, it uses the natural ordering to order the keys, and values mapped to a key.

SortedSetMultimap<String, String> treeMultimap = TreeMultimap.create();
treeMultimap.put("2", "d");
treeMultimap.put("1", "a");
treeMultimap.put("1", "c");
treeMultimap.put("1", "b");
treeMultimap.put("1", "b");
System.out.println(treeMultimap);//{1=[a, b, c], 2=[d]}

Conclusion

In this considerably long post, you got introduced to the Multimap in Google Guava. We saw the APIs or methods of the Multimap interface. Then we learnt about some useful utility methods of the Multimaps class. At last, we saw some implementing classes of the Multimap with examples.

Let me know what you felt about this post by dropping a comment below.
Don’t forget to check on the other posts on Google Guava.

References

The Google Guava wiki
Guide to Guava Multimap
Google Guava Multimap from Techie Delight

Leave a Reply