A Map<K, V> stores key-value pairs where every key is unique and points to one value. Use it whenever your code has to look something up by an identifier: a product by its productId, a customer by their email, an order by its orderId. This lesson covers what Map is (and why it isn't a Collection), the core methods every Java developer should know, the handful of default methods that make Map code much shorter, how to iterate a map cleanly, the key equality rule that decides whether a lookup succeeds, and a one-line tour of the four main implementations.
The first thing to clear up: java.util.Map is not a subinterface of java.util.Collection. Lists, Sets, Queues, and Deques all extend Collection. Map sits on its own branch of the type hierarchy. The reason is that a Collection holds individual elements, but a Map holds pairs, and pairs don't fit the single-element model that methods like add(E e) assume.
What this means in practice: you can't pass a Map to a method that takes Collection, and you can't call map.add(...) because the method doesn't exist. The Map interface has its own method names (put, get, remove) and its own views (keySet, values, entrySet) that let you treat the keys, values, or pairs as Collections when you need to.
The diagram shows the split. Collection and its descendants are on the left. Map is on the right, with its own family of implementations. They share a package (java.util) and a design style, but the inheritance trees don't meet.
A Map has two type parameters: K for the key type and V for the value type. A Map<String, Integer> maps strings to integers; a Map<String, Product> maps strings to product objects. Keys are unique within a single map. If you put a key that already exists, the new value replaces the old one. Values, by contrast, can repeat across different keys.
The core methods are put, get, remove, containsKey, containsValue, and size. They're enough to build almost any lookup the interface is good for.
put(K key, V value) inserts a pair, or replaces the value if the key already exists. It returns the previous value associated with the key, or null if the key wasn't present. get(Object key) returns the value for a key, or null if the key isn't in the map. remove(Object key) deletes the pair and returns the removed value. containsKey and containsValue each return a boolean. size returns the number of pairs.
A small program maps product IDs to in-stock counts. It uses HashMap as the implementation, but every line below this point would work the same for LinkedHashMap or TreeMap.
get("P-999") returns null rather than throwing, because Map treats missing keys as a normal case. That's convenient, but it also means a null return is ambiguous: either the key isn't in the map, or the key is in the map and the value happens to be null. When you need to tell the two cases apart, use containsKey.
put returning the old value matters when you want to know whether a key was already present.
The first put returned 12 because that key was already mapped. The second put returned null because P-102 wasn't in the map before.
HashMap gives you O(1) average-case put, get, and remove. TreeMap gives you O(log n) because it keeps keys sorted. Pick HashMap when you don't care about order; the next few lessons go into the details.
remove and containsValue round out the basic API.
containsValue walks every entry to find a match, so it's O(n). containsKey on a HashMap is O(1) on average. If you find yourself calling containsValue a lot, the data should probably be modeled as a second map, keyed by what you're searching for.
A Map has three views that expose its contents as Collections. keySet() returns a Set<K> of all the keys. values() returns a Collection<V> of all the values (it isn't a Set because values can repeat). entrySet() returns a Set<Map.Entry<K, V>> of all the pairs.
These aren't copies. They're live views backed by the map itself. Adding to or removing from the map shows up in the view, and removing from the view removes from the map. You can't add to a keySet or values view directly, because there's no sensible way to add half a pair, but you can remove from them.
The order of the printed elements is not guaranteed for HashMap. Different runs, or different JDK versions, can produce different orders. If you need a stable order, you'd use LinkedHashMap (insertion order) or TreeMap (sorted key order).
A Map.Entry<K, V> is a small object that pairs a key with a value. It exposes getKey(), getValue(), and setValue(V newValue). You can use setValue to update a value during iteration, which is the one safe way to mutate a map while you're walking it.
The clean idiom for visiting every pair in a map is to iterate entrySet() with an enhanced for loop. It gives you both the key and the value in one step, without the cost of looking the value up by key inside the loop.
There's an alternative pattern that beginners sometimes write: loop over keySet() and call get(key) inside the loop. It works, but it does an extra lookup per iteration that the entrySet version avoids.
Looping keySet plus calling get inside the loop does a hash lookup on every iteration. Looping entrySet reads the key and value together. Prefer entrySet when you need both.
If you only need the keys, loop keySet(). If you only need the values, loop values(). Use entrySet() whenever you need the pair.
Java 8 added forEach, which takes a BiConsumer<K, V> and is concise for small bodies.
The forEach version is fine for read-only work. For anything that needs to mutate the map during iteration (other than via Entry.setValue), stick with entrySet and use the iterator's remove method.
Java 8 added several default methods on Map that replace common patterns. The five that come up most often are getOrDefault, putIfAbsent, computeIfAbsent, compute, and merge.
getOrDefault(key, defaultValue) returns the value for the key, or the default if the key is missing. It replaces the old containsKey then get dance.
getOrDefault does not insert the default into the map. It only returns it. The map is unchanged.
putIfAbsent(key, value) inserts the pair only if the key isn't already present. It returns the existing value, or null if it inserted.
Use computeIfAbsent(key, mappingFunction) when you're building a map-of-lists, a map-of-sets, or any "lazily initialize a value the first time we see this key" pattern. It checks if the key is present; if not, it runs the function to produce a value, stores it, and returns it. If the key is already present, it returns the existing value.
A grouping example: take a list of orders and group them by customer email.
The first time the loop sees "alice@example.com", the map has no entry, so computeIfAbsent calls the lambda, creates a new ArrayList, stores it under the key, and returns it. The .add(orderId) then pushes the order ID into that list. On every subsequent visit to the same key, computeIfAbsent skips the lambda and returns the existing list, so the new order ID gets appended to the list that's already there. Without computeIfAbsent, this is a four-line check-and-create idiom; with it, it's one line.
merge(key, value, mergeFunction) is a concise way to write a counter. If the key is missing, it stores the value. If the key is present, it combines the old value and the new value using the function, and stores the result.
A frequency-counter for product SKUs in a shopping cart (here, SKU is just the product's identifier string):
The first time a SKU appears, merge stores 1. Every subsequent time, it calls Integer::sum on the old count and 1 and stores the new total. The same pattern works for any reduction: summing prices per customer, picking the latest timestamp per session, concatenating strings per group.
compute(key, remappingFunction) is the general form. It takes a function that receives the current value (or null if the key is missing) and returns the new value. If the function returns null, the entry is removed.
For P-101, the lambda received 12 and returned 9, so the entry is updated. For P-102, the lambda received 0 and returned null, which removes the entry. The map ends up with only P-101.
A Map decides whether two keys are "the same" using equals. Hash-based maps like HashMap also use hashCode to find the right bucket. The contract between the two methods is non-negotiable: if a.equals(b) is true, then a.hashCode() == b.hashCode() must also be true. Violating that contract breaks get, containsKey, and remove in ways that are hard to debug.
Built-in key types like String, Integer, Long, and UUID already implement both methods correctly, which is why they're the most common map keys. Custom classes used as keys must override both. For now, the rule is enough:
.equals must have the same hashCode.hashCode must not change while the key is in the map.A consequence: mutable objects are dangerous as keys. If you put a Product into a map and then mutate one of the fields that contributes to its hashCode, the bucket the entry was filed into no longer matches the new hash, and lookups for that key will fail.
The lookup string is a different value at the source level but produces an interned String with the same contents, so .equals returns true and the map finds the entry. Map lookups go through equals, not ==, which is why two String instances with the same characters are interchangeable as keys.
The next four chapters each cover one implementation in depth. A one-liner on each one, so you know what's coming and which one to pick when:
| Implementation | Order | Average get/put | Notes |
|---|---|---|---|
HashMap | None | O(1) | The default choice. Allows one null key and any number of null values. Not thread-safe. |
LinkedHashMap | Insertion order (or access order) | O(1) | Like HashMap but remembers the order keys were inserted. Useful for LRU caches. |
TreeMap | Sorted by key | O(log n) | Keeps keys in sorted order. Supports range queries. Does not allow null keys. |
Hashtable | None | O(1) | Legacy class from Java 1.0. Synchronized, no null keys or values. Avoid in new code; prefer ConcurrentHashMap. |
Pick HashMap unless you have a specific reason not to. Pick LinkedHashMap when iteration order matters. Pick TreeMap when you need keys sorted or you need range queries (smallest key, largest key, all keys between two values). Skip Hashtable in new code.
10 quizzes