HashMap
A HashMap stores key-value pairs. Each key maps to exactly one value, and you retrieve a value by its key rather than by a numeric position. Lookups, insertions, and removals take constant time on average, which makes HashMap one of the most heavily used classes in Java: caches, configuration settings, counting occurrences, indexing records by ID, and grouping data all rely on it.
Creating and Using a HashMap
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> stock = new HashMap<>();
stock.put("pen", 120);
stock.put("notebook", 45);
stock.put("eraser", 80);
System.out.println(stock.get("notebook")); // 45
System.out.println(stock.get("stapler")); // null: no such key
System.out.println(stock.size()); // 3
The two type arguments are the key type and the value type: Map<String, Integer> maps strings to integers. As with other collections, declare the variable with the interface type Map and choose HashMap as the implementation.
Keys are unique, values are not
Putting a value for an existing key replaces the old value:
Map<String, Integer> stock = new HashMap<>();
stock.put("pen", 10);
Integer old = stock.put("pen", 15); // returns the previous value: 10
System.out.println(stock.get("pen")); // 15
Different keys can map to the same value. put returns the previous value for that key, or null if there was none.
Core Methods
| Method | Effect |
|---|---|
put(key, value) | Associate value with key; returns the previous value or null |
get(key) | The value for key, or null if absent |
getOrDefault(key, default) | The value, or default if absent |
containsKey(key) | Whether the key is present |
containsValue(value) | Whether any key maps to this value (slow: scans all values) |
remove(key) | Remove the entry; returns the removed value or null |
putIfAbsent(key, value) | Put only if the key is absent; returns the existing value or null |
size(), isEmpty(), clear() | Count, emptiness, remove everything |
keySet(), values(), entrySet() | Views of the keys, the values, and the key-value pairs |
get and null
get returns null both when the key is absent and when the key maps to null (HashMap allows one null key and any number of null values). Two consequences:
Map<String, Integer> stock = new HashMap<>();
int count = stock.get("ink"); // NullPointerException: unboxing null
int safe = stock.getOrDefault("ink", 0); // 0
Use getOrDefault when absent keys have a sensible default, and containsKey when you must distinguish "absent" from "present with a null value." Avoiding null values altogether makes maps much easier to work with.
Iterating Over a Map
A map is not a Collection, but it provides three collection views:
Map<String, Integer> stock = new HashMap<>(Map.of("pen", 120, "notebook", 45, "eraser", 80));
for (String item : stock.keySet()) {
System.out.println(item);
}
for (int qty : stock.values()) {
System.out.println(qty);
}
for (Map.Entry<String, Integer> e : stock.entrySet()) {
System.out.println(e.getKey() + " = " + e.getValue());
}
stock.forEach((item, qty) -> System.out.println(item + ": " + qty));
When you need both keys and values, iterate over entrySet() (or use forEach). Iterating over keySet() and calling get for each key performs a second lookup per entry for no reason.
Order is not guaranteed
HashMap makes no promise about iteration order; it depends on the keys' hash codes and the table size, and can change as entries are added. Use:
LinkedHashMapto iterate in insertion order,TreeMapto iterate in sorted key order (next lesson).
Modifying while iterating
Adding or removing entries directly while iterating over a view throws ConcurrentModificationException (usually). Use removeIf on a view, or the iterator's remove:
stock.values().removeIf(qty -> qty == 0); // remove out-of-stock items
stock.entrySet().removeIf(e -> e.getKey().startsWith("x"));
Updating values with put for keys already in the map is safe, since it does not change the structure.
Updating Values: merge, compute, and Friends
Common update patterns used to require several lines:
// Old style: count occurrences
Integer c = counts.get(word);
counts.put(word, c == null ? 1 : c + 1);
Since Java 8, single methods express these patterns directly:
Map<String, Integer> counts = new HashMap<>();
for (String word : "to be or not to be".split(" ")) {
counts.merge(word, 1, Integer::sum);
}
System.out.println(counts.get("to")); // 2
| Method | Behavior |
|---|---|
merge(key, value, fn) | If absent, put value; otherwise put fn(oldValue, value). If fn returns null, the entry is removed. |
computeIfAbsent(key, fn) | If absent, compute a value with fn(key), store it, and return it; otherwise return the existing value |
computeIfPresent(key, fn) | If present, replace the value with fn(key, oldValue) |
compute(key, fn) | Always compute a new value from fn(key, oldValueOrNull) |
replace(key, value) | Replace only if the key is present |
computeIfAbsent is the standard way to build a map of collections, for example grouping items:
Map<String, List<String>> byCity = new HashMap<>();
byCity.computeIfAbsent("Chennai", k -> new ArrayList<>()).add("Ada");
byCity.computeIfAbsent("Chennai", k -> new ArrayList<>()).add("Grace");
byCity.computeIfAbsent("Pune", k -> new ArrayList<>()).add("Alan");
// {Chennai=[Ada, Grace], Pune=[Alan]} (order may vary)
The list is created only the first time a key appears; later calls return the existing list, and .add appends to it.
How HashMap Works
A HashMap holds an array of buckets. To store or find a key:
- It calls
key.hashCode()and mixes the high bits into the low bits (h ^ (h >>> 16)) to spread values evenly. - It reduces the result to a bucket index by keeping only as many low bits as the table size needs (the table size is always a power of two).
- It looks only in that bucket, using
key.equals(...)to find the matching entry.
put("pen", 120) with the default table of 16 buckets
"pen".hashCode() → 110873
mix: 110873 ^ (110873 >>> 16) → 110872
index: 110872 & 15 → bucket 8
bucket 8: [("pen", 120)]
get("pen") → bucket 8 → compare keys with equals → 120
Because only one bucket is examined, the cost does not depend on how many entries the map holds. Keys that land in the same bucket form a short list; if a bucket becomes large, it is converted into a balanced tree so performance degrades gracefully. When the map is about 75% full (the default load factor), the table doubles in size and entries are redistributed, so the same key may then land in a different bucket.
For a map that will hold a known number of entries, HashMap.newHashMap(expectedSize) (Java 19+) creates a table large enough to avoid resizing.
Keys need correct equals and hashCode
Everything in the previous lesson about HashSet applies to map keys (a HashSet is in fact built on a HashMap):
- Key classes must override both
equalsandhashCode, consistently.String, wrapper types, enums,LocalDate, and records already do. - Keys must not be mutated in ways that change their hash code while they are in the map, or the entry becomes unreachable.
record Coordinate(int row, int col) { }
Map<Coordinate, String> board = new HashMap<>();
board.put(new Coordinate(0, 0), "X");
System.out.println(board.get(new Coordinate(0, 0))); // X: records compare by value
Values have no such requirement; they can be any object.
Immutable Maps
Map<String, Integer> limits = Map.of("free", 1, "pro", 5, "team", 25);
Map<String, Integer> copy = Map.copyOf(someMap);
Map.of and Map.copyOf create unmodifiable maps. They reject null keys and values and throw UnsupportedOperationException on modification. Map.of accepts up to 10 key-value pairs; for more, use Map.ofEntries(Map.entry(k, v), ...). To get a modifiable map from one, wrap it: new HashMap<>(Map.of(...)).
HashMap Versus Other Maps
HashMap | LinkedHashMap | TreeMap | |
|---|---|---|---|
| Iteration order | None guaranteed | Insertion (or access) order | Sorted by key |
get / put / remove | O(1) average | O(1) average | O(log n) |
| Key requirements | equals + hashCode | equals + hashCode | Comparable or a Comparator |
null key | One allowed | One allowed | Not with natural ordering |
| Extra features | Can act as an LRU cache | firstKey, floorKey, ranges |
For use from multiple threads, HashMap is not safe; use ConcurrentHashMap (covered in the multithreading section).
Practical Example: Order Statistics
import java.util.*;
public class OrderStats {
record Order(String customer, String city, double amount) { }
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("ada", "Chennai", 1200),
new Order("linus", "Bengaluru", 450),
new Order("ada", "Chennai", 800),
new Order("grace", "Chennai", 3000),
new Order("linus", "Bengaluru", 1550),
new Order("alan", "Pune", 999)
);
Map<String, Double> spendByCustomer = new HashMap<>();
Map<String, Integer> ordersByCity = new HashMap<>();
Map<String, Set<String>> customersByCity = new HashMap<>();
for (Order o : orders) {
spendByCustomer.merge(o.customer(), o.amount(), Double::sum);
ordersByCity.merge(o.city(), 1, Integer::sum);
customersByCity.computeIfAbsent(o.city(), c -> new TreeSet<>()).add(o.customer());
}
List<Map.Entry<String, Double>> ranking = new ArrayList<>(spendByCustomer.entrySet());
ranking.sort(Map.Entry.<String, Double>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()));
System.out.println("Top customers:");
for (Map.Entry<String, Double> e : ranking) {
System.out.printf(" %-6s %8.2f%n", e.getKey(), e.getValue());
}
for (String city : new TreeSet<>(ordersByCity.keySet())) {
System.out.println(city + ": " + ordersByCity.get(city) + " orders from " + customersByCity.get(city));
}
System.out.println("Orders from Mumbai: " + ordersByCity.getOrDefault("Mumbai", 0));
}
}
Output:
Top customers:
grace 3000.00
ada 2000.00
linus 2000.00
alan 999.00
Bengaluru: 2 orders from [linus]
Chennai: 3 orders from [ada, grace]
Pune: 1 orders from [alan]
Orders from Mumbai: 0
One pass over the orders builds three maps. merge with Double::sum accumulates each customer's spending, and with Integer::sum counts orders per city. computeIfAbsent creates a TreeSet for a city the first time it appears, then adds customers to it, removing duplicates and keeping names sorted. Because HashMap has no order, the ranking copies the entries into a list and sorts them by value, descending, with the key as a tie-breaker so that equal totals (ada and linus) always appear in the same order. Cities are printed by iterating over a sorted copy of the key set, and getOrDefault reports zero for a city with no orders instead of returning null.