Java HashMap
HashMap is the most-used implementation of the Map interface. It stores key-value pairs with O(1) average lookup, insertion, and deletion. Keys must be unique; values may be duplicated. A key's hashCode and equals determine equality and bucket placement.
This tutorial covers creation, put/get/remove, iteration, the important equals/hashCode contract, the modern Map.of factories, and the computeIfAbsent family of methods that replace a lot of ugly boilerplate.
1. Creating a HashMap
import java.util.HashMap;
import java.util.Map;
class=class="tok-str">"tok-cmt">// empty
Map<String, Integer> ages = new HashMap<>();
class=class="tok-str">"tok-cmt">// with initial capacity
Map<String, String> cache = new HashMap<>(class="tok-num">256);
class=class="tok-str">"tok-cmt">// from existing map
Map<String, Integer> copy = new HashMap<>(ages);
class=class="tok-str">"tok-cmt">// immutable (Java class="tok-num">9+)
Map<String, Integer> week = Map.of(
"Mon", class="tok-num">1, "Tue", class="tok-num">2, "Wed", class="tok-num">3, "Thu", class="tok-num">4, "Fri", class="tok-num">5
);
class=class="tok-str">"tok-cmt">// using Map.ofEntries for more than class="tok-num">10 pairs
import static java.util.Map.entry;
Map<String, Integer> months = Map.ofEntries(
entry("Jan", class="tok-num">1), entry("Feb", class="tok-num">2), entry("Mar", class="tok-num">3),
entry("Apr", class="tok-num">4), entry("May", class="tok-num">5), entry("Jun", class="tok-num">6),
entry("Jul", class="tok-num">7), entry("Aug", class="tok-num">8), entry("Sep", class="tok-num">9),
entry("Oct", class="tok-num">10), entry("Nov", class="tok-num">11), entry("Dec", class="tok-num">12)
);
2. Putting and Getting
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", class="tok-num">30);
ages.put("Bob", class="tok-num">25);
ages.put("Carol", class="tok-num">40);
class=class="tok-str">"tok-cmt">// get
int aliceAge = ages.get("Alice"); class=class="tok-str">"tok-cmt">// class="tok-num">30
Integer unknown = ages.get("Dave"); class=class="tok-str">"tok-cmt">// null - key not present
class=class="tok-str">"tok-cmt">// get with default
int daveAge = ages.getOrDefault("Dave", -class="tok-num">1); class=class="tok-str">"tok-cmt">// -class="tok-num">1
class=class="tok-str">"tok-cmt">// put returns the previous value (null if absent)
Integer previous = ages.put("Alice", class="tok-num">31); class=class="tok-str">"tok-cmt">// class="tok-num">30, now stored class="tok-num">31
class=class="tok-str">"tok-cmt">// putIfAbsent - only if key is absent
ages.putIfAbsent("Bob", class="tok-num">99); class=class="tok-str">"tok-cmt">// no change, Bob already class="tok-num">25
Putting a key that already exists overwrites the previous value and returns it. Putting a new key returns null.
3. Removing and Checking
Map<String, Integer> ages = new HashMap<>(Map.of("Alice",class="tok-num">30,"Bob",class="tok-num">25));
class=class="tok-str">"tok-cmt">// remove by key
Integer removed = ages.remove("Bob"); class=class="tok-str">"tok-cmt">// class="tok-num">25
class=class="tok-str">"tok-cmt">// remove only if value matches
boolean ok = ages.remove("Alice", class="tok-num">30); class=class="tok-str">"tok-cmt">// true if value was class="tok-num">30
class=class="tok-str">"tok-cmt">// contains
boolean has = ages.containsKey("Alice"); class=class="tok-str">"tok-cmt">// true
boolean hasV = ages.containsValue(class="tok-num">30); class=class="tok-str">"tok-cmt">// true
class=class="tok-str">"tok-cmt">// sizes and emptiness
int n = ages.size(); class=class="tok-str">"tok-cmt">// class="tok-num">1
boolean empty = ages.isEmpty(); class=class="tok-str">"tok-cmt">// false
class=class="tok-str">"tok-cmt">// iterate keys or values
Set<String> keys = ages.keySet();
Collection<Integer> vals = ages.values();
Set<Map.Entry<String,Integer>> entries = ages.entrySet();
4. Iterating
Map<String, Integer> ages = Map.of("Alice", class="tok-num">30, "Bob", class="tok-num">25, "Carol", class="tok-num">40);
class=class="tok-str">"tok-cmt">// class="tok-num">1. entrySet - the canonical iteration
for (Map.Entry<String, Integer> e : ages.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
class=class="tok-str">"tok-cmt">// class="tok-num">2. keySet only
for (String name : ages.keySet()) {
System.out.println(name);
}
class=class="tok-str">"tok-cmt">// class="tok-num">3. values only
for (int age : ages.values()) {
System.out.println(age);
}
class=class="tok-str">"tok-cmt">// class="tok-num">4. forEach with a BiConsumer
ages.forEach((name, age) -> System.out.println(name + " is " + age));
class=class="tok-str">"tok-cmt">// class="tok-num">5. stream of entries
ages.entrySet().stream()
.filter(e -> e.getValue() >= class="tok-num">30)
.forEach(e -> System.out.println(e.getKey()));
Calling map.put or map.remove inside a for-each over entrySet throws ConcurrentModificationException. Use iterator.remove(), or collect changes into a list and apply after the loop.
5. The equals / hashCode Contract
HashMap uses hashCode to find a bucket, then equals to find the right entry within the bucket. If two keys are equals, they must have the same hashCode. If you break the rule, the map will misplace entries:
class=class="tok-str">"tok-cmt">// BAD: overrides equals but not hashCode - HashMap will misplace entries
public class Person {
private final String name;
public Person(String name) { this.name = name; }
@Override public boolean equals(Object o) {
return o instanceof Person p && p.name.equals(this.name);
}
class=class="tok-str">"tok-cmt">// NO hashCode override - uses Object's identity-based hashCode
}
Map<Person, Integer> map = new HashMap<>();
map.put(new Person("Alice"), class="tok-num">1);
map.get(new Person("Alice")); class=class="tok-str">"tok-cmt">// null - different hashCode, different bucket!
class=class="tok-str">"tok-cmt">// GOOD: record satisfies the contract automatically
public record PersonRec(String name) {}
Map<PersonRec, Integer> map2 = new HashMap<>();
map2.put(new PersonRec("Alice"), class="tok-num">1);
map2.get(new PersonRec("Alice")); class=class="tok-str">"tok-cmt">// class="tok-num">1 - works
Records satisfy the contract automatically. For regular classes, IDEs generate correct hashCode and equals in one keystroke. Never override one without the other.
6. computeIfAbsent, merge
The classic Java 7 pattern for a frequency counter is verbose:
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
Integer count = counts.get(word);
if (count == null) {
counts.put(word, class="tok-num">1);
} else {
counts.put(word, count + class="tok-num">1);
}
}
Java 8 added the compute family, which collapse this into one line:
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
counts.merge(word, class="tok-num">1, Integer::sum);
}
class=class="tok-str">"tok-cmt">// or the older computeIfAbsent form
for (String word : words) {
counts.computeIfAbsent(word, k -> class="tok-num">0);
counts.put(word, counts.get(word) + class="tok-num">1);
}
class=class="tok-str">"tok-cmt">// or with compute (one-pass)
for (String word : words) {
counts.compute(word, (k, v) -> v == null ? class="tok-num">1 : v + class="tok-num">1);
}
merge is even cleaner for accumulators: map.merge(key, 1, Integer::sum) adds 1 to the current value (or starts from 1 if absent).
7. Immutable Maps
class=class="tok-str">"tok-cmt">// up to class="tok-num">10 pairs
Map<String, Integer> small = Map.of("a", class="tok-num">1, "b", class="tok-num">2);
class=class="tok-str">"tok-cmt">// class="tok-num">11+ pairs via Map.ofEntries
import static java.util.Map.entry;
Map<String, Integer> big = Map.ofEntries(
entry("one", class="tok-num">1), entry("two", class="tok-num">2), entry("three", class="tok-num">3),
entry("four", class="tok-num">4), entry("five", class="tok-num">5), entry("six", class="tok-num">6),
entry("seven", class="tok-num">7), entry("eight", class="tok-num">8), entry("nine", class="tok-num">9),
entry("ten", class="tok-num">10), entry("eleven", class="tok-num">11)
);
class=class="tok-str">"tok-cmt">// any of these throw UnsupportedOperationException on modification
class=class="tok-str">"tok-cmt">// small.put("z", class="tok-num">26); // throws
For more than 10 entries, use Map.ofEntries(Map.entry(...), ...). The resulting map is immutable and is the preferred way to return a constant lookup table from a method.
8. Performance and Capacity
| Operation | Average | Worst (collisions) |
|---|---|---|
put | O(1) | O(log n) (Java 8+ treeifies buckets) |
get | O(1) | O(log n) |
remove | O(1) | O(log n) |
containsKey | O(1) | O(log n) |
containsValue | O(n) | O(n) — linear scan of all values |
If you know the expected size, pass it to the constructor: new HashMap<>(expectedSize / 0.75 + 1). The default load factor is 0.75 — the map resizes when it is 75% full.
Exercises
- Build a frequency counter for words in a sentence using
merge. - Create a
Map<Integer, String>of HTTP status codes withMap.ofand print each entry. - Use
computeIfAbsentto lazily cache the factorial of a number. - Make a class with two fields, override
hashCodeandequalscorrectly, and use it as a HashMap key. Confirm retrieval works.