Guava's Multimap
Guava's Multimap interface and its implementations give Java developers a new tool. They replace Map<X, List<Y>>, as well as Map<X, Set<Y>>, both of which can be a little awkward and verbose.
Multimap Summary
For those in a hurry, here's a quick summary of how to use a Multimap.
// Initializing Multimap<String, String> mm = HashMultimap.create(); // Insert in a single step. mm.putAll("be", Sets.newHashSet("am", "are", "is", "was", "were")); // Add items individually. mm.put("ask", "ask"); mm.put("ask", "asks"); mm.put("ask", "asked"); // get( ) mm.get("be"); mm.get("ask"); // iterate through all the entries for(Entry<String, String> mapPair : mm.entries()) { String key = mapPair.getKey(); String value = mapPair.getValue(); }
Advantages of Multimap. Here are some of the advantages a multimap has over, for example, Map<X, List<Y>>.
- You don't have to test whether the key exists in the map before inserting new values.
- You can query on a non-existent map key without worrying about a null pointer exception.
- You can iterate through all values without first obtaining the list associated with each key.
- With one method call, in O(1) time, you can get the number of all entries in the map.
That's the summary. The next sections explore Multimaps in the detail they deserve, expanding on the examples given above.
Multimap Functionality
Creating and populating. For the sake of illustration, suppose we want to associate a number of simple forms of an English verb with their infinitive form. Without Multimap, you would probably use a Map<String, Set<String>>, where the key was the infinitive form, and the set associated with each key consisted of all the various forms for that verb.
With Guava, you would use a Multimap<String, String> instead. The lines below show how to create the multimap and load it with the forms for two verbs, ask and be.
// Instantiate the map. Multimap<String, String> mm = HashMultimap.create(); // Insert items individually. mm.put("ask", "ask"); mm.put("ask", "asks"); mm.put("ask", "asked"); // Insert in a two-step process. Set<String> beList = Sets.newHashSet("am", "are", "is", "was", "were"); mm.putAll("be", beList); // Insert in a single step. mm.putAll("have", Sets.newHashSet("have", "has", "had")); // Or allow duplicate values with: Multimap<String, String> mm2 = ArrayListMultimap.create();
Listing 2 shows that you can load each list element individually, with put( ), or collectively, with putAll( ). Significantly, in neither case do you have to worry about whether the map already contains some verb forms that you don't want to lose.
Using Multimap with arrays instead of sets. Note the commented-out line at the end, which shows how to instantiate a Multimap that allows repeated values that behave like an ArrayList, rather than values that have HashSet-like behavior.
Multimap get( ). Now suppose you've populated your map and you want to do something useful with it.
// Getting particular entries. Set<String> expected = Sets.newHashSet("am", "are", "is", "was", "were"); assertEquals(expected, mm.get("be")); assertEquals(0, mm.get("do").size());
Listing 3 uses unit tests to show how to get the Multimap values for a given key. First we verify that calling get("be")
on the multimap we created in the previous listing returns the expected forms of the verb "be." The second assertEquals( ) statement sees how many entries the multimap contains for the key "do." In this case, there are none. Here we see a significant advantage that multimap has over Map<X, List<Y>>, where get("do")
would return null, and calling size( ) on that would give you nothing but a lousy NullPointerException.
Iterating. Let's move on now to iteration.
// Iterate through all the entries for(Entry<String, String> mapPair : mm.entries()) { String key = mapPair.getKey(); String value = mapPair.getValue(); System.out.println("key = " + key + "; value = " + value); } Output: key = ask; value = ask key = ask; value = asks key = ask; value = asked key = be; value = am key = be; value = are key = be; value = is key = be; value = was key = be; value = were
The above code snippet shows that to iterate through a multimap, you don't have to retrieve every key separately and then iterate through all the elements of its list. The entries( ) method does this work for you.
The useful toString( ) override. The next listing shows the multimap's toString( ) override, which allows you to see all the keys and their values without doing any iteration yourself.
System.out.println("Write using toString():"); System.out.println(mm.toString()); Output: Write using toString(): {ask=[ask, asks, asked], be=[am, are, is, was, were]}
Getting just the values, without the keys. Perhaps you're completely indifferent about the keys, and all you want is the elements of all the lists. The next example gets all the elements with a method call to values( ); then, just for the fun of it, the code snippet goes on to sort those entries and write out the result.
// Getting all entries when you don't care about the key. Collection<String> valuesAsCollection = mm.values(); // Sort the values. List<String> valuesAsList = Lists.newArrayList(valuesAsCollection); Collections.sort(valuesAsList); System.out.println(valuesAsList); Output: [am, are, ask, asked, asks, is, was, were]
Getting the count of all entries. Suppose you want a count of all the entries in your multimap. The snippet below shows you, first, how to do this with Multimap; then it shows you how you would have to do this if you used the traditional Map<X, Set<Y>>.
// With Guava size = mm2.size(); assertEquals(5, size); // Without Guava, using a Map<X, Set<Y>>, requires a loop int size = 0; for(String key : mm1.keySet()) { size = size + mm1.get(key).size(); } assertEquals(5, size);
Other map-like methods. In addition to size( ), which is shown above, the Multimap interface offers you other methods you expect from an ordinary map. The next listing demonstrates some of these.
// Testing the values for the key "be". // containsKey( ) assertTrue(mm.containsKey("be")); // get( ) Collection<String> beForms = mm.get("be"); assertEquals(5, beForms.size()); // containsEntry( ) assertTrue(mm.containsEntry("be", "was")); // keySet( ) Set<String> keys = mm.keySet(); assertEquals(2, keys.size());
Multimap to Multiset. Finally, the keys( ) method returns another Guava interface called Multiset. The next listing gives you just a taste of what a Multiset can do for you. See the link at right for more.
Multiset<String> multiset = mm.keys(); int size = multiset.size(); assertEquals(8, size); assertEquals(5, multiset.count("be")); assertEquals(3, multiset.count("ask"));