Java HashMap

INTERMEDIATE ~8 min read Tutorial

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

java
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

java
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

java
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

java
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()));
Do not modify the map while iterating

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:

java
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:

java
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:

java
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

java
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

OperationAverageWorst (collisions)
putO(1)O(log n) (Java 8+ treeifies buckets)
getO(1)O(log n)
removeO(1)O(log n)
containsKeyO(1)O(log n)
containsValueO(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

  1. Build a frequency counter for words in a sentence using merge.
  2. Create a Map<Integer, String> of HTTP status codes with Map.of and print each entry.
  3. Use computeIfAbsent to lazily cache the factorial of a number.
  4. Make a class with two fields, override hashCode and equals correctly, and use it as a HashMap key. Confirm retrieval works.