The Java Developer's Guide to Maps
Overview #
In this article, I’ll explore how maps work in Java, including updates and new features introduced up to Java 21.
What is a Map? #
A map is a data structure that’s designed for fast lookups. Data is stored in key-value pairs with every key being unique. Each key maps to a value hence the name. These pairs are called map entries.
In the JDK, java.util.Map is the interface that includes method signatures for insertion, removal, and retrieval.
Collections Framework #
The Map interface is implemented by a number of classes in the Collections Framework. Each class offers different functionality and thread safety. The most common implementation is the HashMap so I’ll be using this for most of our examples.
Map is the only collection that doesn’t extend or implement the Collection interface. It doesn’t fit the same contract since it needs to work with pairs instead of single values.
Create a Map #
The keys and values of a map can be any reference type. Primitive types are not supported because of a restriction around the way generics were designed.
A HashMap allows one null key and multiple null values. It doesn’t preserve the order of the elements and doesn’t guarantee the order will remain the same over time.
Let’s create a HashMap with String keys and values.
Map<Integer, String> map = new HashMap<>();
Since all maps implement the Map interface, the following methods will work for any of the map implementations illustrated above.
Add to a Map #
The put()
method allows us to insert entries into our map. It requires two parameters: a key and its value.
Now, let’s populate our map with ids and names:
map.put(1, "Canon");
map.put(2, "Nikon");
map.put(3, "Sony");
map.put(4, "Fuji");
Here’s what our map looks like now:
Key | Value |
---|---|
1 | Canon |
2 | Nikon |
3 | Sony |
4 | Fuji |
Sometimes we may want to add multiple entries in bulk or combine two maps. There’s a method that handles this called putAll()
. It copies the entry references from another map in order to populate ours.
Duplicate Keys #
I mentioned before that duplicate keys aren’t allowed in maps.
Let’s see what happens when I try to insert a key that already exists:
map.put(4, "Leica");
The method doesn’t complain, but notice how the new value overwrote the previous one:
Key | Value |
---|---|
1 | Canon |
2 | Nikon |
3 | Sony |
4 | Leica |
The put()
method returns the previous value if we want to use it. If there was no previous value, it returns null.
To check whether a key already exists, I can use the containsKey()
boolean method:
if (map.containsKey(4)) {
throw new IllegalArgumentException("Unable to add manufacturer due to duplicate id");
}
Similarly, the containsValue()
method checks for existence of a value:
boolean containsRicoh = map.containsValue("Ricoh"); // false
Retrieve from a Map #
The get()
method accepts a key and returns the value associated with that key or null if there is no value.
String value = map.get(2); // Nikon
Note
Don’t confuse map keys with zero-based indexes used in arrays. There is no key 0 in the map I created.
Remove from a Map #
The remove()
method accepts a key so it can find the entry to remove. It returns the value associated with the removed entry or null if there is no value.
String mapValue = map.remove(3); // removes and returns "Sony"
mapValue = map.remove(3); // removes nothing because key 3 no longer exists, returns null
Key | Value |
---|---|
1 | Canon |
2 | Nikon |
4 | Leica |
If we want to empty the map, we can call clear()
. This is a void method so it does not return anything.
map.clear();
Map Size #
The size()
method returns number of entries in our map.
int size = map.size();
The isEmpty()
method returns a boolean indicating if the map is empty or not.
if (map.isEmpty()) {
// do something when map is empty
}
Collection Views #
The Map interface provides Collection view methods which allow us to view our map in terms of a collection type. These views provide the only mechanism for us to iterate over a map.
keySet()
- returns a Set of keys from the mapvalues()
- returns a Collection of values from the mapentrySet()
- returns a Set ofMap.Entry
objects which represent the key-value pairs in the map
It’s important to remember that views are backed by our map. This means any changes we make to the view update the underlying map and vice versa.
The collection views support removal of entries but not insertion.
keySet() #
The keySet()
method returns a Set
view of the keys contained in our map:
Set<Integer> keys = map.keySet(); // [1, 2, 4]
values() #
The values()
method returns a Collection of the values contained in our map:
Collection<String> values = map.values(); // [Canon, Nikon, Leica]
Why does it return a Collection
instead of a Set
? Because the values of a map aren’t guaranteed to be unique so a Set
wouldn’t work.
entrySet() #
The entrySet()
method is used to get a Set
view of the entries in our map. The Set will contain Map.Entry
objects. A Map.Entry object is simply a key-value pair. I can then call getKey()
or getValue()
on this object.
The most common usage of the entrySet()
is for looping which I’ll cover in the next section.
Looping over a Map #
There are many ways of iterating over a map. Let’s go over some common approaches.
Keep in mind that loops will throw a NullPointerException if we try to iterate over a map that is null.
Using foreach and Map.Entry #
This is the most common method and is preferable in most cases. I get access to both keys and values in the loop.
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println( entry.getKey() + " -> " + entry.getValue() );
}
Using Java 8 Lambda expression #
map.forEach((k,v) -> System.out.println("key: " + k + ", value: " + v));
Sort a Map by Key #
A HashMap
does not preserve any order in its entri
Map<String, String> capitals = new HashMap<>();
capitals.put("United States", "Washington, D.C.");
capitals.put("Canada", "Ottawa");
capitals.put("Mexico", "Mexico City");
capitals.put("Bahamas", "Nassau");
capitals.forEach((k,v) -> System.out.println("key: " + k + ", value: " + v));
key: Canada, value: Ottawa
key: United States, value: Washington, D.C.
key: Mexico, value: Mexico City
key: Bahamas, value: Nassau
If we use a TreeMap
, it will automatically sort the entries by key. Let’s convert our HashMap
to a TreeMap
and print the contents:
Map<String, String> sortedCapitals = new TreeMap<>();
sortedCapitals.putAll(capitals);
sortedCapitals.forEach((k,v) -> System.out.println("key: " + k + ", value: " + v));</code></pre>
The map is now sorted by country name:
key: Bahamas, value: Nassau
key: Canada, value: Ottawa
key: Mexico, value: Mexico City
key: United States, value: Washington, D.C.
Sort a Map by Value #
This one is a little trickier and requires us to write a method.
Before Java 8 #
|
|
- On line 3, we create a list named entries which contains Map.Entry objects from the map we passed into the method.
- On lines 5-10, we use the
Collections.sort()
and implement a Comparator usingentry.getValue()
so it knows how to compare the entries. Our list is now sorted by value. But we need to get it back into a map. - On line 12, we create a LinkedHashMap which is a map implementation that preserves the insertion order of its entries.
- On lines 14-16, we iterate over our list and populate the new map.
Java 8+ #
Java 8 introduced the Stream API which makes this a bit easier.
|
|
Let’s see this method in action:
Map<String, String> capitals = new HashMap<>();
capitals.put("United States", "Washington, D.C.");
capitals.put("Canada", "Ottawa");
capitals.put("Mexico", "Mexico City");
capitals.put("Bahamas", "Nassau");
Map<String, String> sortedCapitals = sortByValue(capitals);
sortedCapitals.forEach((k,v) -> System.out.println("key: " + k + ", value: " + v));
The output is now sorted by capital city:
key: Mexico, value: Mexico City
key: Bahamas, value: Nassau
key: Canada, value: Ottawa
key: United States, value: Washington, D.C.
Updates in Java 9 to 21 #
Immutable Collections (Java 9) #
Java 9 introduced convenient factory methods for creating immutable collections, including maps. For instance, Map.of
and Map.ofEntries
allow for the creation of small maps without the need for an explicit constructor:
var immutableMap = Map.of("key1", "value1", "key2", "value2");
immutableMap.remove("key1"); // throws java.lang.UnsupportedOperationException
Enhancements in the Map Interface #
Subsequent Java versions have continued to enhance the Map interface and its implementations:
- Java 10 and Java 11 focused on performance optimizations and minor API improvements.
- Java 12 to 21 introduced more methods for conditional updates, improved null handling, and other utility methods enhancing the Map interface’s usability and flexibility.
Handling Duplicate Keys with MultiMap #
While traditional maps disallow duplicate keys, there are scenarios where storing multiple values per key is necessary. Here, Google Guava’s MultiMap
comes into play, offering an elegant solution.
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
Multimap<String, String> foods = ArrayListMultimap.create();
foods.put("Fruits", "Apple");
foods.put("Fruits", "Banana");
foods.put("Fruits", "Grape");
foods.put("Vegetables", "Carrot");
Retrieving values for a key yields a collection:
Collection<String> fruits = foods.get("Fruits"); // [Apple, Banana, Grape]
Conclusion #
This guide has been updated to reflect the latest changes and features introduced in Java. Maps remain a fundamental part of the Java Collections Framework, with ongoing enhancements to make them more powerful and easier to use.