Blog

Java HashMap

Key Takeaways

  • Java HashMap stores key/value pairs and provides expected O(1) lookup and insertion when hashes are well distributed.
  • Its iteration order is unspecified. Choose LinkedHashMap for insertion order or TreeMap for sorted keys.
  • Equal keys must have equal hash codes. Don't change a stored key's equality or hashing behavior.
  • HashMap isn't thread-safe. Prefer ConcurrentHashMap for typical shared, mutable maps.

Java HashMap is a general-purpose implementation of the Map interface that stores values under unique keys. It's a good default when you need fast key-based lookup and don't require an iteration order.

What Is a HashMap?

A map associates each key with at most one value, although different keys can share the same value. With HashMap, put adds a mapping or replaces an existing key's value; get retrieves the value associated with a key.

This complete example preserves the basic people lookup and the scoring example. Save it as HashMapBasics.java.

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

public class HashMapBasics {
    public static void main(String[] args) {
        Map<String, Integer> people = new HashMap<>();
        people.put("Sam", 45);
        people.put("Brenda", 32);
        int age = people.get("Sam");
        System.out.println(age); // 45

        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 100);
        scores.put("Bob", 85);
        int aliceScore = scores.get("Alice");
        System.out.println(aliceScore); // 100
    }
}

Declaring the variables as Map keeps the calling code independent of the particular implementation. You can change the construction to an ordered map without changing these put/get calls.

Null Keys, Null Values, and Missing Entries

HashMap permits one null key and multiple null values. A null result from get can mean either an absent key or a key explicitly mapped to null; containsKey distinguishes those cases.

Decide whether null is meaningful in your application before using it as a value. Otherwise, missing entries and deliberately empty values become unnecessarily difficult to distinguish.

When Should You Use a HashMap?

Use HashMap for lookups such as customer IDs to customers, names to scores, or words to counts when order isn't part of the requirement. Choose the map's guarantees first, rather than treating every Map implementation as interchangeable.

ImplementationIteration orderNull supportConcurrent access
HashMapUnspecifiedOne null key; multiple null valuesNot synchronized
LinkedHashMapInsertion order by default; optional access orderOne null key; multiple null valuesNot synchronized
TreeMapSorted by keysNull values allowed; null keys require a comparator that supports themNot synchronized
ConcurrentHashMapUnspecifiedNo null keys or valuesThread-safe operations

LinkedHashMap normally preserves insertion order. Its three-argument constructor enables access order when the accessOrder argument is true, ordering entries from least recently accessed to most recently accessed. TreeMap instead uses natural key ordering or a supplied Comparator and guarantees O(log n) get and put operations. These are different ordering requirements, not merely different performance settings.

Java HashMap Implementation

Hashing and Bucket Selection

In OpenJDK 25, HashMap uses a power-of-two bucket array. For a non-null key, it spreads h = key.hashCode() with h ^ (h >>> 16), then selects the bucket using (capacity - 1) & hash. It checks stored hashes and key equality within that bucket.

A collision means different keys select the same bucket, even if their hash codes differ. Collision bins usually contain linked nodes; sufficiently large bins can become balanced trees, a feature introduced in Java 8.

key --> hashCode --> spread hash --> bucket index
                                      |
                         +------------+------------+
                         |            |            |
                      empty       linked bin    tree bin
                                  A --> B          D
                                                 / \
                                                C   E

Illustration only: labels aren't sorted map keys.

Collisions, Tree Bins, and Time Complexity

OpenJDK 25 defines TREEIFY_THRESHOLD as 8 and MIN_TREEIFY_CAPACITY as 64. A long bin can trigger treeification, but a smaller table resizes instead. These thresholds are implementation details, not Map API guarantees.

Tree bins improve collision handling, but lookup can still degrade to O(n) for arbitrary, non-orderable keys with identical hashes. Don't turn expected O(1) performance into a worst-case promise.

For a deeper walkthrough, see our Java HashMap implementation guide.

Capacity, Load Factor, and Resizing

Capacity is the number of buckets; size is the number of mappings. The default initial capacity is 16 and the default load factor is 0.75. Growth normally occurs when the entry count exceeds capacity multiplied by load factor—not when that percentage of buckets is occupied.

Growth rebuilds the table with approximately twice as many buckets, so an insertion that triggers resizing costs more than an ordinary insertion. Excessive capacity also has a cost: iteration takes time proportional to capacity plus size.

When you know the expected entry count, use HashMap.newHashMap(int), available since Java 19. It accounts for the default load factor when choosing an initial capacity suitable for that many mappings.

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

// Method-body fragment; requires Java 19 or later.
Map<String, Integer> scores = HashMap.newHashMap(1_000);

With older constructors, estimate capacity from expected entries divided by load factor, rounded up. The constructor's capacity argument isn't an expected-entry count. Keep the default load factor unless measurement gives you a reason to change it.

How equals and hashCode Affect HashMap Keys

The contract is straightforward: if a.equals(b) is true, a.hashCode() and b.hashCode() must match. The reverse isn't required—unequal objects can have the same hash code. If you write a key class yourself, implement both methods consistently.

A record is convenient for a small value-based key because Java generates component-based equals and hashCode methods. This example uses a String and an int as the key's components.

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

public class HashMapKeys {
    record EmployeeKey(String department, int id) {}

    public static void main(String[] args) {
        Map<EmployeeKey, String> names = new HashMap<>();
        names.put(new EmployeeKey("engineering", 7), "Sam");

        System.out.println(
            names.get(new EmployeeKey("engineering", 7))
        ); // Sam
    }
}

Don't mutate information used by a stored key's equals or hashCode methods: later lookups may no longer find its mapping. Records aren't deeply immutable, so a record containing a mutable collection still needs care. See our explanation of equals() and hashCode() in Java for the underlying contract.

Java HashMap Examples Beyond put and get

The Map API provides useful operations for defaults, initialization, and aggregation. Their null behavior matters:

  • getOrDefault(key, fallback) uses the fallback only when the key is absent; an explicit null mapping still returns null.
  • putIfAbsent(key, value) installs the value when the key is absent or currently mapped to null.
  • computeIfAbsent(key, function) computes a value for an absent or null-mapped key, but doesn't install a new value if the function returns null.
  • merge(key, value, function) installs the supplied non-null value for an absent or null-mapped key; otherwise it combines the existing and supplied values. A null result from its remapping function removes the mapping.

Here, merge counts occurrences while computeIfAbsent groups names by their first letter. Save the complete example as HashMapPatterns.java.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HashMapPatterns {
    public static void main(String[] args) {
        List<String> names = List.of("Sam", "Brenda", "Sam");
        Map<String, Integer> counts = new HashMap<>();
        Map<String, List<String>> byInitial = new HashMap<>();

        for (String name : names) {
            counts.merge(name, 1, Integer::sum);
            String initial = name.substring(0, 1);
            byInitial.computeIfAbsent(initial, key -> new ArrayList<>())
                     .add(name);
        }

        System.out.println(counts.get("Sam")); // 2
        System.out.println(byInitial.get("S")); // [Sam, Sam]
    }
}

Keep computation callbacks focused on producing the value; they shouldn't modify the same map during computation. Find additional patterns in our Java HashMap examples.

Java HashMap vs Hashtable

Hashtable is a legacy map with synchronized methods and no support for null keys or values. Its documentation recommends HashMap when synchronization isn't needed and ConcurrentHashMap when a highly concurrent implementation is needed.

Synchronized individual methods don't make a sequence such as get followed by put atomic: another thread can act between those calls. Don't summarize the choice as “Hashtable is slower”; performance depends on your workload and contention. Our HashMap vs Hashtable comparison explores the selection tradeoffs.

ConcurrentHashMap and Atomic Updates

For a shared counter, ConcurrentHashMap.merge performs the update atomically for that mapping. That doesn't turn several calls or updates across multiple keys into a transaction. Keep remapping functions short because competing updates may block while they run.

import java.util.concurrent.ConcurrentHashMap;

// Method-body fragment; each merge is an atomic update.
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
counts.merge("Sam", 1, Integer::sum);

Synchronizing a HashMap Correctly

Collections.synchronizedMap is another option when you need a synchronized wrapper. Access the backing map only through the wrapper, and synchronize on that returned map—not its views—during iteration.

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

// Method-body fragment.
Map<String, Integer> people =
    Collections.synchronizedMap(new HashMap<>());
people.put("Sam", 45);

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

HashMap's fail-fast iterators detect structural modification on a best-effort basis. ConcurrentModificationException is a debugging signal, not a mechanism for making concurrent access safe.

Production Checklist

  • Choose explicit ordering when output order matters to callers or tests.
  • Use stable keys, decide on null semantics, and size from realistic entry counts.
  • Bound map growth when accepting untrusted input; don't treat collision handling as a substitute for resource limits.
  • For shared state, define which updates must be atomic and benchmark with realistic keys and contention.

FAQ

Is HashMap thread-safe?

No. Use ConcurrentHashMap for typical concurrent updates, or coordinate access through appropriate synchronization. A Collections.synchronizedMap wrapper requires synchronization on the returned map while iterating its views.

Does HashMap maintain insertion order?

No. HashMap's iteration order is unspecified, even if it appears stable in a particular run. LinkedHashMap preserves insertion order by default; TreeMap provides sorted-key order.

Can a HashMap contain null keys or values?

Yes, HashMap permits one null key and multiple null values. Use containsKey to distinguish a missing key from a key mapped to null. Hashtable and ConcurrentHashMap reject null keys and values.

What happens if two keys have the same hashCode?

Unequal keys can coexist in a HashMap even when their hash codes match; key equality distinguishes their mappings. Colliding entries occupy the same bucket, typically using linked nodes or, under implementation-specific conditions, a balanced tree.

When should I use ConcurrentHashMap instead of HashMap?

Use ConcurrentHashMap when multiple threads need to read and update the same mutable map without a single external lock around ordinary map operations. Use its atomic update methods rather than assuming a sequence of separate calls is atomic.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me