Java HashMap

A HashMap is Java’s general-purpose key-value store: it lets you associate a unique key with a value and retrieve that value almost instantly, no matter how many entries are stored. It lives in java.util and implements the Map interface. Because lookups, insertions, and deletions all run in constant time on average, HashMap is the default choice whenever you need to look things up by a key — counting word frequencies, caching computed results, indexing records by ID, and countless other everyday tasks.

Overview: How HashMap Works

A HashMap<K, V> stores entries as key-value pairs. Internally, it keeps an array of "buckets" (a plain array of linked list heads, by default 16 slots). When you call put(key, value), Java computes key.hashCode(), mixes those bits with a supplemental hash function to spread them more evenly, and uses the result modulo the array length to pick a bucket index. The key-value pair is stored in that bucket as a small internal object (a Node) holding the hash, the key, the value, and a reference to the next node in the same bucket (for collisions).

When two different keys hash to the same bucket — a collision — they are chained together in that bucket’s linked list. On lookup, Java finds the correct bucket by hash, then walks the chain comparing each stored key to the requested key with equals() until it finds a match. As long as collisions are rare, this is effectively O(1). In the worst case (many keys landing in one bucket), Java 8 and later automatically converts a bucket’s linked list into a small red-black tree once it holds more than 8 entries (and the table is large enough), which caps worst-case lookup at O(log n) instead of O(n).

HashMap automatically grows. It tracks a load factor (default 0.75) and a threshold equal to capacity × loadFactor. Once the number of entries exceeds that threshold, the internal array doubles in size and every existing entry is rehashed into the new, larger array — an operation called resizing. This keeps buckets short on average as the map grows, at the cost of an occasional expensive rehash.

Two properties matter enormously for correctness: keys must have consistent equals() and hashCode() implementations, and iteration order is unspecified. Unlike arrays or lists, a HashMap makes no promise about the order entries come back in when you iterate — that order depends on hash values and internal bucket layout, and can change after a resize. If you need predictable order, use LinkedHashMap (insertion order) or TreeMap (sorted order) instead. HashMap also allows exactly one null key and any number of null values, and it is not thread-safe — concurrent modification from multiple threads can corrupt its internal structure; use ConcurrentHashMap for multithreaded code.

Syntax

Declaring and creating a HashMap looks like this:

HashMap<KeyType, ValueType> name = new HashMap<>();
Constructor Description
new HashMap<>() Empty map, default capacity 16, default load factor 0.75
new HashMap<>(int initialCapacity) Empty map with a chosen starting capacity (rounded up to a power of two)
new HashMap<>(int initialCapacity, float loadFactor) Full control over capacity and the resize threshold
new HashMap<>(Map<? extends K, ? extends V> m) Copies all entries from another map

Commonly used methods:

Method Purpose
put(K key, V value) Insert or overwrite the value for a key; returns the previous value or null
get(Object key) Return the value for a key, or null if absent
getOrDefault(Object key, V default) Return the value, or a fallback if the key is missing
containsKey(Object key) / containsValue(Object value) Membership tests
remove(Object key) Delete an entry, returning its value or null
putIfAbsent(K key, V value) Insert only if the key isn’t already present
merge(K key, V value, BiFunction remap) Combine a new value with any existing one (great for counters)
computeIfAbsent(K key, Function mapper) Compute and store a value only if the key is missing
keySet() / values() / entrySet() Views for iterating keys, values, or key-value pairs
size() / isEmpty() / clear() Basic bookkeeping

Examples

Example 1: Basic Operations

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        HashMap<Integer, String> students = new HashMap<>();
        students.put(101, "Priya");
        students.put(102, "Marcus");
        students.put(103, "Wei");

        System.out.println("Student 102: " + students.get(102));
        System.out.println("Contains 105? " + students.containsKey(105));
        System.out.println("Name or default: " + students.getOrDefault(105, "Unknown"));

        students.put(102, "Marcus Lee"); // overwrites existing value
        students.remove(103);

        System.out.println("Size: " + students.size());
        for (Map.Entry<Integer, String> entry : students.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

Output:

Student 102: Marcus
Contains 105? false
Name or default: Unknown
Size: 2
101 -> Priya
102 -> Marcus Lee

This shows the core operations: inserting with put, reading with get, checking existence with containsKey, providing a fallback with getOrDefault, overwriting a key by calling put again, and removing an entry. Note that get(102) is read before the overwrite, so it still returns the original value "Marcus".

Example 2: Word Frequency Counter

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class Main {
    public static void main(String[] args) {
        String text = "the quick brown fox jumps over the lazy dog the fox runs";
        String[] words = text.split(" ");

        HashMap<String, Integer> frequency = new HashMap<>();
        for (String word : words) {
            frequency.merge(word, 1, Integer::sum);
        }

        // Sort by key just for predictable, readable output
        TreeMap<String, Integer> sorted = new TreeMap<>(frequency);
        for (Map.Entry<String, Integer> entry : sorted.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

Output:

brown: 1
dog: 1
fox: 2
jumps: 1
lazy: 1
over: 1
quick: 1
runs: 1
the: 3

This is a realistic use of HashMap: counting occurrences. merge(word, 1, Integer::sum) inserts 1 if the word is new, or adds 1 to the existing count otherwise — a one-line replacement for a manual "check if present, then increment" block. Since a plain HashMap‘s own iteration order is unspecified, the entries are copied into a TreeMap before printing so the output is alphabetically sorted and predictable.

Example 3: Custom Keys Need equals() and hashCode()

import java.util.HashMap;

public class Main {
    static class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof Point)) return false;
            Point p = (Point) o;
            return this.x == p.x && this.y == p.y;
        }

        @Override
        public int hashCode() {
            return 31 * x + y;
        }
    }

    public static void main(String[] args) {
        HashMap<Point, String> labels = new HashMap<>();
        labels.put(new Point(1, 2), "Treasure");

        Point lookup = new Point(1, 2); // a different object, same coordinates
        System.out.println("Found: " + labels.get(lookup));
        System.out.println("Same object? " + (lookup == labels.keySet().iterator().next()));
    }
}

Output:

Found: Treasure
Same object? false

Because Point overrides both equals() and hashCode() based on its x and y fields, a brand-new Point instance with the same coordinates hashes to the same bucket and compares equal to the stored key, so get() finds it — even though it is a completely different object in memory.

Under the Hood: Step by Step

  • Hashing: put/get call key.hashCode(), then XOR the result with its own bits shifted right 16 places. This "spreads" high-order bits into the low-order bits, so keys that differ only in their high bits don’t all collide.
  • Bucket selection: the spread hash is combined with (capacity - 1) using a bitwise AND to compute an array index. Because capacity is always a power of two, this is a fast substitute for the modulo operator.
  • Collision handling: if the bucket already has entries, Java walks the chain, comparing hashes first (cheap) and then equals() (only if hashes match) to find an existing key to update, or appends a new node if none matches.
  • Resizing: once size exceeds capacity × loadFactor, the array doubles and every entry is rehashed into the new array. This is why bulk-inserting into a map you can size up front (via the initial-capacity constructor) avoids repeated, wasted rehashing.
  • Treeification: if a single bucket grows beyond 8 entries and the table itself is large enough, that bucket’s linked list is converted into a balanced red-black tree, bounding worst-case lookup time even against pathological hash collisions.

Common Mistakes

Mistake 1: Forgetting to override equals() and hashCode()

If a custom key class relies on the default, identity-based Object.equals() and Object.hashCode(), then two "equivalent" objects are treated as completely different keys:

import java.util.HashMap;

public class Main {
    static class BadPoint {
        int x, y;
        BadPoint(int x, int y) { this.x = x; this.y = y; }
        // no equals()/hashCode() override -- uses Object's identity-based versions
    }

    public static void main(String[] args) {
        HashMap<BadPoint, String> labels = new HashMap<>();
        labels.put(new BadPoint(1, 2), "Treasure");

        BadPoint lookup = new BadPoint(1, 2);
        System.out.println("Found: " + labels.get(lookup));
    }
}

Output:

Found: null

Even though both BadPoint objects represent the same coordinates, the map can’t tell — it falls back to reference identity, so the lookup misses. The fix is exactly what Example 3 showed: override both equals() and hashCode() consistently, based on the same fields, whenever a class is used as a map key.

Mistake 2: Modifying a map while iterating over it directly

Removing entries from a map through its own remove() method while a for-each loop is iterating over it throws a ConcurrentModificationException, because the loop’s iterator detects the map changed underneath it:

import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        HashMap<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 10);
        scores.put("Bob", 20);
        scores.put("Charlie", 30);

        try {
            for (String key : scores.keySet()) {
                scores.remove(key); // modifying the map while iterating over it
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Caught: " + e.getClass().getSimpleName());
        }
        System.out.println("Size after failed loop: " + scores.size());

        // Correct way: remove through the iterator itself
        Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
        while (it.hasNext()) {
            it.next();
            it.remove(); // safe -- the iterator knows about its own change
        }
        System.out.println("Size after safe removal: " + scores.size());
    }
}

Output:

Caught: ConcurrentModificationException
Size after failed loop: 2
Size after safe removal: 0

The first loop removes one entry successfully, then throws as soon as the iterator’s next step detects the map changed outside of its control. The second loop uses Iterator.remove(), which tells the iterator about the change, so it can safely continue — this is the correct pattern for removing entries mid-iteration.

Best Practices

  • Always override equals() and hashCode() together for any class used as a key — never one without the other.
  • Prefer immutable key objects (like String or a class with final fields); mutating a key after it’s inserted can make it unfindable, since it may now hash to a different bucket.
  • Never rely on HashMap iteration order. Use LinkedHashMap for insertion order or TreeMap for sorted order if order matters.
  • Use getOrDefault, putIfAbsent, merge, and computeIfAbsent to replace verbose "check-then-act" boilerplate with a single call.
  • If you know roughly how many entries you’ll store, pass an initial capacity to the constructor to avoid repeated resizing.
  • Never modify a map’s structure during a for-each loop; use Iterator.remove(), or collect changes and apply them afterward.
  • Use ConcurrentHashMap instead of HashMap when multiple threads read and write the same map.

Practice Exercises

  • Write a program that reads a sentence and uses a HashMap<Character, Integer> to count how many times each vowel (a, e, i, o, u) appears, ignoring case.
  • Given parallel arrays of employee names and salaries, build a HashMap<String, Double> and print the name of the employee with the highest salary.
  • Implement a memoized factorial function using a HashMap<Integer, Long> as a cache with computeIfAbsent, so repeated calls for the same input skip recomputation.

Summary

  • HashMap<K, V> stores key-value pairs and offers average O(1) put/get/remove by hashing keys into buckets.
  • Keys are located using hashCode() to pick a bucket and equals() to confirm a match — both must be overridden consistently for custom key classes.
  • The map resizes (doubles capacity and rehashes) once it exceeds capacity × loadFactor (default 0.75).
  • Iteration order is unspecified and can change after a resize; use LinkedHashMap or TreeMap if order matters.
  • HashMap allows one null key and multiple null values, and is not thread-safe.
  • Never structurally modify a map while iterating except through the iterator’s own remove() method.