Skip to content
Java collections 7 min read

HashMap

HashMap is Java’s most popular key-value store. It maps unique keys to values, lets you look up any value in near-instant time, and handles a huge range of problems — from counting word frequencies to building simple in-memory caches.

What is HashMap?

HashMap<K, V> lives in java.util and implements the Map interface. Every entry is a key → value pair. Keys must be unique; values can repeat. It allows one null key and any number of null values.

import java.util.HashMap;

public class BasicMap {
    public static void main(String[] args) {
        HashMap<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob",   87);
        scores.put("Carol", 92);

        System.out.println(scores.get("Bob"));   // 87
        System.out.println(scores);
    }
}

Output:

87
{Alice=95, Bob=87, Carol=92}

Note: HashMap does not guarantee insertion order. If you need predictable order, use LinkedHashMap (insertion order) or TreeMap (sorted order).

Creating a HashMap

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

public class CreateMap {
    public static void main(String[] args) {
        // 1. Default constructor (initial capacity 16, load factor 0.75)
        HashMap<String, Integer> map1 = new HashMap<>();

        // 2. Custom initial capacity
        HashMap<String, Integer> map2 = new HashMap<>(32);

        // 3. Custom capacity + load factor
        HashMap<String, Integer> map3 = new HashMap<>(32, 0.5f);

        // 4. Copy from another map (Map.of is immutable; copy to make mutable)
        Map<String, Integer> source = Map.of("x", 1, "y", 2);
        HashMap<String, Integer> map4 = new HashMap<>(source);

        System.out.println(map4);
    }
}

Output:

{x=1, y=2}

Core Operations

put, get, and containsKey

import java.util.HashMap;

public class CoreOps {
    public static void main(String[] args) {
        HashMap<String, String> capitals = new HashMap<>();

        // Add / update entries
        capitals.put("France", "Paris");
        capitals.put("Japan",  "Tokyo");
        capitals.put("India",  "New Delhi");

        // Read
        System.out.println(capitals.get("Japan"));           // Tokyo
        System.out.println(capitals.get("Germany"));         // null (missing key)

        // Check existence
        System.out.println(capitals.containsKey("France"));  // true
        System.out.println(capitals.containsValue("Rome"));  // false

        // Overwrite
        capitals.put("India", "Mumbai"); // wrong, but shows overwrite
        System.out.println(capitals.get("India"));           // Mumbai
    }
}

Output:

Tokyo
null
true
false
Mumbai

remove and size

import java.util.HashMap;

public class RemoveOps {
    public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        map.put(1, "One");
        map.put(2, "Two");
        map.put(3, "Three");

        map.remove(2);                  // remove by key
        map.remove(3, "Wrong value");   // conditional remove — does nothing here

        System.out.println(map.size()); // 2
        System.out.println(map);        // {1=One, 3=Three}
    }
}

Output:

2
{1=One, 3=Three}

Iterating a HashMap

There are four common ways to walk through a HashMap.

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

public class IterateMap {
    public static void main(String[] args) {
        HashMap<String, Integer> pop = new HashMap<>();
        pop.put("Tokyo",    14000000);
        pop.put("Delhi",    32941000);
        pop.put("Shanghai", 28516904);

        // 1. entrySet() — most efficient, gives both key & value
        for (Map.Entry<String, Integer> entry : pop.entrySet()) {
            System.out.println(entry.getKey() + " → " + entry.getValue());
        }

        // 2. keySet()
        for (String city : pop.keySet()) {
            System.out.println(city);
        }

        // 3. values()
        for (int population : pop.values()) {
            System.out.println(population);
        }

        // 4. forEach (lambda, Java 8+)
        pop.forEach((city, p) -> System.out.println(city + ": " + p));
    }
}

Tip: Prefer entrySet() when you need both key and value — it avoids the second hash lookup that get(key) inside a keySet() loop would require.

Useful Modern Methods (Java 8+)

HashMap gained several powerful methods in Java 8 that eliminate common boilerplate.

import java.util.HashMap;

public class ModernMethods {
    public static void main(String[] args) {
        HashMap<String, Integer> wordCount = new HashMap<>();

        // getOrDefault — safe fallback instead of null-check
        wordCount.put("hello", 3);
        System.out.println(wordCount.getOrDefault("hello",  0)); // 3
        System.out.println(wordCount.getOrDefault("world",  0)); // 0

        // putIfAbsent — only inserts if key is missing
        wordCount.putIfAbsent("hello", 99);  // ignored, key exists
        wordCount.putIfAbsent("java",   1);  // inserted
        System.out.println(wordCount.get("hello")); // 3
        System.out.println(wordCount.get("java"));  // 1

        // merge — great for counting / accumulating
        String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
        HashMap<String, Integer> freq = new HashMap<>();
        for (String w : words) {
            freq.merge(w, 1, Integer::sum);
        }
        System.out.println(freq); // {apple=3, banana=2, cherry=1}

        // computeIfAbsent — create value lazily
        HashMap<String, java.util.List<String>> groups = new HashMap<>();
        groups.computeIfAbsent("fruits", k -> new java.util.ArrayList<>()).add("mango");
        groups.computeIfAbsent("fruits", k -> new java.util.ArrayList<>()).add("kiwi");
        System.out.println(groups); // {fruits=[mango, kiwi]}
    }
}

Output:

3
0
3
1
{apple=3, banana=2, cherry=1}
{fruits=[mango, kiwi]}

Null Keys and Values

import java.util.HashMap;

public class NullKeys {
    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<>();
        map.put(null, "null key is allowed");
        map.put("key1", null);            // null value is allowed
        map.put("key2", null);            // multiple null values fine

        System.out.println(map.get(null));   // null key is allowed
        System.out.println(map.get("key1")); // null
    }
}

Warning: Having a null key means map.get(null) returning null is ambiguous — is the key missing, or is the value actually null? Use containsKey(null) to distinguish the two cases.

HashMap vs Hashtable vs LinkedHashMap vs TreeMap

FeatureHashMapHashtableLinkedHashMapTreeMap
Thread-safeNoYes (synchronized)NoNo
Null keys1 allowedNot allowed1 allowedNot allowed
OrderNoneNoneInsertion orderSorted (natural/comparator)
PerformanceO(1) avgO(1) avg (+ lock)O(1) avgO(log n)
SinceJava 1.2Java 1.0Java 1.4Java 1.2

For thread-safe use cases, prefer ConcurrentHashMap from java.util.concurrent over the legacy Hashtable.

Under the Hood

Understanding the internals helps you write faster code and avoid surprises.

Hash Table Structure

A HashMap internally holds an array of buckets (called Node[] table). When you call put(key, value):

  1. Java calls key.hashCode() and applies an additional spread function ((h = key.hashCode()) ^ (h >>> 16)) to reduce collisions from poor hash functions.
  2. The result is bit-masked to an array index: index = hash & (capacity - 1).
  3. The entry is stored in the bucket at that index.

If two keys hash to the same bucket (collision), they form a linked list at that slot (chaining). Since Java 8, once a bucket’s list grows beyond 8 entries, it converts to a Red-Black Tree, keeping worst-case lookup at O(log n) rather than O(n).

Capacity and Load Factor

The default initial capacity is 16 and the default load factor is 0.75. When the number of entries exceeds capacity × loadFactor (i.e., 12 entries in a fresh map), the table rehashes — it doubles in capacity and redistributes every entry. Rehashing is expensive (O(n)), so if you know you’ll store many items, pass an estimated capacity to the constructor to avoid multiple resizes:

// Storing ~100 entries: pre-size to avoid rehash at 12, 24, 48, 96
HashMap<String, Integer> map = new HashMap<>(128); // next power-of-two above 100/0.75

hashCode and equals Contract

HashMap relies on two methods from Object:

  • hashCode() — determines the bucket
  • equals() — resolves identity within a bucket

If you use a custom object as a key, you must override both consistently:

import java.util.HashMap;
import java.util.Objects;

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

    @Override
    public int hashCode() { return Objects.hash(x, y); }

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

public class CustomKeyMap {
    public static void main(String[] args) {
        HashMap<Point, String> map = new HashMap<>();
        map.put(new Point(1, 2), "origin-ish");

        // Works because equals+hashCode are consistent
        System.out.println(map.get(new Point(1, 2))); // origin-ish
    }
}

Output:

origin-ish

Warning: If you override equals() but not hashCode(), two “equal” objects will land in different buckets and get() will return null even though the key logically exists. This is one of the most common bugs with custom keys. See Object Class for the full contract.

Memory Layout

Each entry is a Node<K,V> object containing: hash (int), key, value, and next (pointer to next node in the same bucket). A map with many collisions wastes memory via these linked nodes. Choosing good key types (like String, Integer) with well-distributed hash codes keeps the map lean and fast.

Common Patterns

Frequency Counter

import java.util.HashMap;

public class FrequencyCounter {
    public static void main(String[] args) {
        int[] nums = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
        HashMap<Integer, Integer> freq = new HashMap<>();
        for (int n : nums) {
            freq.merge(n, 1, Integer::sum);
        }
        System.out.println(freq);
    }
}

Output:

{1=2, 2=1, 3=2, 4=1, 5=3, 6=1, 9=1}

Grouping (one-to-many)

import java.util.*;

public class Grouping {
    public static void main(String[] args) {
        String[] words = {"ant", "bat", "arc", "bear", "bell", "cap"};
        HashMap<Character, List<String>> byFirst = new HashMap<>();
        for (String w : words) {
            byFirst.computeIfAbsent(w.charAt(0), k -> new ArrayList<>()).add(w);
        }
        byFirst.forEach((ch, list) -> System.out.println(ch + " → " + list));
    }
}

Output:

a → [ant, arc]
b → [bat, bear, bell]
c → [cap]
Last updated June 13, 2026
Was this helpful?