The Collections Framework

HashMap in Depth

15 min Lesson 6 of 14

HashMap in Depth

A HashMap is the most-used implementation of the Map interface in Java. It stores data as key-value pairs, gives you O(1) average-case lookup by key, and is the right tool whenever you need to associate one thing with another — a username to a profile, a word to its frequency, a product id to its price.

The Map Interface

Before touching HashMap directly, understand what the Map interface promises. Unlike List or Set, a Map is not a Collection — it has its own type hierarchy. The contract says:

  • Every key maps to exactly one value.
  • Keys are unique; putting the same key twice replaces the old value.
  • Values need not be unique — multiple keys can map to the same value.
  • A key can map to null, and null itself is a valid key in HashMap.
Map is not a Collection. Map<K,V> does not extend Collection<E>. You cannot pass a Map where a Collection is expected. You can, however, call map.values(), map.keySet(), or map.entrySet() to get collection views.

Creating a HashMap

Always declare with the interface type on the left so you can swap implementations later:

import java.util.HashMap; import java.util.Map; Map<String, Integer> wordCount = new HashMap<>();

The diamond operator <> lets the compiler infer the type arguments. You can also seed a map at construction time in Java 9+:

// Immutable factory — useful for small fixed maps Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 82, "Carol", 78);

Core Operations: put, get, remove

put(key, value) adds or replaces an entry and returns the previous value (or null if the key was absent). get(key) returns the value or null. remove(key) deletes the entry and returns the removed value.

Map<String, Integer> stock = new HashMap<>(); stock.put("apples", 50); stock.put("bananas", 30); stock.put("apples", 60); // replaces 50 — key "apples" now maps to 60 Integer prev = stock.put("oranges", 20); // prev is null (new key) int apples = stock.get("apples"); // 60 Integer missing = stock.get("grapes"); // null — key not present stock.remove("bananas"); // map now has apples=60, oranges=20 System.out.println(stock.size()); // 2 System.out.println(stock.containsKey("apples")); // true System.out.println(stock.containsValue(100)); // false
Avoid unboxing null. stock.get("grapes") returns null. Assigning it to a primitive int (instead of Integer) causes a NullPointerException. Use getOrDefault or an explicit null check when the key might be absent.

Safer Gets: getOrDefault and computeIfAbsent

getOrDefault(key, fallback) returns the fallback instead of null when the key is missing. computeIfAbsent is the idiomatic way to build a value only when needed — perfect for grouping:

// word-frequency counter String[] words = {"cat", "dog", "cat", "bird", "dog", "cat"}; Map<String, Integer> freq = new HashMap<>(); for (String word : words) { freq.put(word, freq.getOrDefault(word, 0) + 1); } // {cat=3, dog=2, bird=1} // group words by first letter using computeIfAbsent Map<Character, List<String>> byLetter = new HashMap<>(); for (String word : words) { byLetter.computeIfAbsent(word.charAt(0), k -> new ArrayList<>()).add(word); } // {c=[cat, cat, cat], d=[dog, dog], b=[bird]}
computeIfAbsent is concise and thread-safe in ConcurrentHashMap. Prefer it over the get-null-check-put pattern when building grouped or nested structures.

Key Uniqueness and equals / hashCode

HashMap uses a key's hashCode() to pick a bucket, then equals() to confirm identity. This means:

  • Two objects that are equals() must have the same hashCode().
  • If you override equals() in a custom class without overriding hashCode(), the map will silently create duplicate keys.
  • A key must never change while it is in the map — mutating a key after insertion breaks lookup.
// Safe: String and Integer are immutable, hashCode is well-defined. Map<String, String> capitals = new HashMap<>(); capitals.put("France", "Paris"); capitals.put("France", "Lyon"); // replaces — "France".equals("France") is true System.out.println(capitals.get("France")); // Lyon

Iterating Entries

There are three common ways to iterate a HashMap. The cleanest is entrySet(), which gives you both key and value together without a second lookup:

Map<String, Integer> population = new HashMap<>(); population.put("Tokyo", 13960000); population.put("Cairo", 9540000); population.put("London", 8982000); // 1. entrySet — most efficient when you need both key and value for (Map.Entry<String, Integer> entry : population.entrySet()) { System.out.println(entry.getKey() + " -> " + entry.getValue()); } // 2. keySet — when you only need keys (or are ok with a get per key) for (String city : population.keySet()) { System.out.println(city); } // 3. forEach with a lambda (Java 8+) population.forEach((city, pop) -> System.out.printf("%s: %,d%n", city, pop));
Iteration order is not guaranteed. HashMap does not preserve insertion order. If you need predictable order use LinkedHashMap (insertion order) or TreeMap (sorted by key) — both covered in the next lesson.

Performance and Initial Capacity

Under the hood, HashMap stores entries in an array of buckets. When the number of entries exceeds capacity * loadFactor (default 0.75), it rehashes — allocating a larger array and redistributing all entries. Rehashing is O(n) and expensive. If you already know the approximate size, pass it at construction to avoid rehashing:

// Expected ~1000 entries — pre-size to avoid rehashing Map<String, Object> cache = new HashMap<>(1400); // 1000 / 0.75 ≈ 1334 → round up

putIfAbsent and merge

Two more methods worth knowing before moving on:

  • putIfAbsent(key, value) — inserts only when the key is absent; returns the existing value otherwise.
  • merge(key, value, remappingFn) — if absent, inserts the value; if present, applies the function to combine old and new values. Very handy for aggregation.
Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 10); scores.putIfAbsent("Alice", 99); // ignored — Alice already present scores.putIfAbsent("Bob", 20); // inserted // accumulate scores with merge scores.merge("Alice", 5, Integer::sum); // Alice: 10 + 5 = 15 scores.merge("Carol", 8, Integer::sum); // Carol: 8 (new key) System.out.println(scores); // {Alice=15, Bob=20, Carol=8}

Summary

HashMap is the workhorse key-value store of the Java Collections Framework. Use put/get/remove for basic operations, getOrDefault and computeIfAbsent for safe access and building nested structures, and entrySet() with a for-each or forEach lambda for iteration. Remember that key uniqueness is enforced via equals() and hashCode() — always override both together in custom key classes.