☕ Java

Java HashMap

A complete guide to Java HashMap — internal bucket-array hashing, Java 8 treeification, all Map methods, null key and value handling, load factor and rehashing, HashMap vs Hashtable vs LinkedHashMap vs TreeMap, thread-safety considerations, and best practices with real-world examples.

📅

Last Updated

March 2026

⏱️

Read Time

19 min

🎯

Level

Beginner to Intermediate

What is Java HashMap?

Java HashMap is the most widely used implementation of the Map<K, V> interface in the Java Collections Framework. Introduced in Java 1.2, it stores data as key-value pairs inside an internal array of buckets (nodes), using a hash function to compute where each entry belongs. It provides O(1) average-case time complexity for get() and put() operations — making it the default go-to Map for everyday Java development.

Unlike its ancestor Hashtable, HashMap is not synchronized and therefore not inherently thread-safe. It is designed for single-threaded use or scenarios where external synchronization is managed by the developer. This deliberate design choice eliminates lock overhead and makes it significantly faster than Hashtable in single-threaded workloads.

HashMap stands apart from other Map implementations with two key permissions: it allows exactly one null key (always stored at bucket index 0) and any number of null values. It does not maintain insertion order (use LinkedHashMap for that) and does not sort keys (use TreeMap for that). Iteration order is undefined and can change after a rehash.

A landmark improvement came in Java 8: when a single bucket's linked-list chain exceeds 8 nodes (TREEIFY_THRESHOLD) and the overall table capacity is at least 64, that chain is automatically converted into a Red-Black Tree. This brings worst-case lookup for hash-colliding keys from O(n) down to O(log n) — a critical fix for adversarial inputs designed to create hash collisions.

Internal Structure — How HashMap Works Under the Hood

HashMap internally maintains an array of Node objects. Each Node is the head of a singly linked list — or, in Java 8+, a Red-Black Tree — of entries that map to the same bucket index. This collision-resolution strategy is called separate chaining.

☕ JavaHashMap Internal Node Structure (Simplified from JDK Source)
// Simplified representation of HashMap's internal Node (Java 8+)
static class Node<K, V> implements Map.Entry<K, V> {
    final int    hash;   // cached hash of the key
    final K      key;    // the key — may be null (stored at index 0)
    V            value;  // the value — may be null
    Node<K,V>    next;   // next node in the same bucket chain
}

// When chain length > TREEIFY_THRESHOLD (8) AND table.length >= 64:
// Node<K,V> is promoted to TreeNode<K,V> — a Red-Black Tree node
static final class TreeNode<K, V> extends LinkedHashMap.Entry<K, V> {
    TreeNode<K,V> parent, left, right, prev;
    boolean red;
}

// Core internal state of HashMap:
transient Node<K,V>[] table;     // the bucket array (length always power of two)
transient int         size;       // total number of key-value entries
int                   threshold;  // size at which rehash occurs (capacity × loadFactor)
final float           loadFactor; // default 0.75f

// Key constants:
static final int DEFAULT_INITIAL_CAPACITY = 16;   // must be power of two
static final int MAXIMUM_CAPACITY         = 1 << 30;
static final float DEFAULT_LOAD_FACTOR    = 0.75f;
static final int TREEIFY_THRESHOLD        = 8;    // chain → tree
static final int UNTREEIFY_THRESHOLD      = 6;    // tree → chain
static final int MIN_TREEIFY_CAPACITY     = 64;   // table must be this big to treeify

How put(key, value) works step by step:

📊 DiagramHashMap Internal Bucket Layout — put("name", "Ravi")
  Step 1: Null check         →  if (key == null) → store at index 0 (special path)
  Step 2: Hash spreading     →  hash = key.hashCode() ^ (key.hashCode() >>> 16)
                                (XOR high bits into low bits to improve distribution)
  Step 3: Find bucket index  →  index = hash & (table.length - 1)  (fast bitwise AND)
  Step 4: Check bucket chain →  scan linked list / tree for key using equals()
            If key found        →  update value, return old value
            If not found        →  append new Node to chain / insert into tree
  Step 5: Check treeify      →  if chain length ≥ TREEIFY_THRESHOLD (8)
                                AND table.length ≥ MIN_TREEIFY_CAPACITY (64)
                                → convert chain to Red-Black Tree
  Step 6: Check threshold    →  if ++size > threshold → resize() [double capacity]

  Bucket Array (capacity = 16):
  ┌──────┐
  │   0  │ → [null key entry]  ← only null key goes here
  │   1  │ → null
  │   2  │ → ["city"="Mumbai"] → null
  │   3  │ → null
  │   4  │ → ["name"="Ravi"] → ["dept"="Finance"] → null  ← chain (collision)
  │   5  │ → null
  │   6  │ → ["id"="201"] → null
  │  ... │
  │  15  │ → null
  └──────┘

  After Java 8 treeification (if bucket 4 chain grows > 8 AND table.length >= 64):
  │   4  │ → [Red-Black Tree root] → O(log n) lookup instead of O(n)

Why power-of-two capacity? HashMap always keeps its bucket array length as a power of two (16, 32, 64, …). This allows the bucket index to be computed with an ultra-fast bitwise AND — index = hash & (capacity - 1) — instead of the slower modulo operation (%) used by Hashtable. The hash spreading step (h ^ (h >>> 16)) compensates for the fact that low-bit patterns in power-of-two tables can cause clustering with poor hashCode() implementations.

HashMap Class Hierarchy

HashMap sits cleanly within the modern Java Collections Framework hierarchy, extending AbstractMap and implementing the Map interface — with no legacy baggage unlike Hashtable.

java.lang.Object
Root of all Java classes
java.util.AbstractMap<K,V> (Abstract class — Java 1.2)
Provides skeletal implementation of the Map interfaceImplements equals(), hashCode(), toString() for all Map subclasses
java.util.Map<K,V> (Interface — Java 1.2)
Core map contract: put, get, remove, containsKey, containsValue, keySet, values, entrySet, forEach, compute, merge
java.util.HashMap<K,V>
Concrete class — power-of-two bucket array with separate chainingExtends AbstractMap + Implements Map, Cloneable, SerializableNot synchronized — single-threaded use or external synchronizationJava 8+: treeification of long chains into Red-Black Trees
java.util.LinkedHashMap<K,V> (Subclass of HashMap)
Maintains a doubly-linked list across all entries to preserve insertion order (or access order)Ideal when iteration order must match insertion sequence

Architecture Diagram

Notable subclass: java.util.LinkedHashMap extends HashMap and adds a doubly-linked list that threads through all entries, preserving either insertion order (default) or access order (when constructed with accessOrder=true). LinkedHashMap is the backbone of LRU cache implementations in Java.

Creating a HashMap in Java

HashMap provides four constructors. Choosing the right one — especially pre-sizing for a known workload — can eliminate unnecessary rehashing and improve performance.

☕ JavaCreating HashMap — All Constructors
import java.util.*;

public class CreateHashMap {
    public static void main(String[] args) {

        // 1. Default constructor: capacity=16, loadFactor=0.75
        HashMap<String, Integer> hm1 = new HashMap<>();

        // 2. Specify initial capacity
        //    Useful when approximate size is known in advance
        HashMap<String, String> hm2 = new HashMap<>(50);

        // 3. Specify both initial capacity and load factor
        //    loadFactor=0.6 → rehash at 60% fill (fewer collisions, more memory)
        HashMap<String, Double> hm3 = new HashMap<>(100, 0.6f);

        // 4. Initialize from an existing Map
        Map<String, Integer> source = new Hashtable<>();
        source.put("Java",   1);
        source.put("Kotlin", 2);
        HashMap<String, Integer> hm4 = new HashMap<>(source);
        System.out.println("From map: " + hm4);

        // Best practice: declare as Map interface reference
        Map<String, String> config = new HashMap<>();
        config.put("host", "localhost");
        config.put("port", "9090");
        config.put("env",  "production");
        System.out.println("Config: " + config);

        // ✅ null key is allowed — stored at bucket index 0
        hm1.put(null, 99);
        System.out.println("Null key value: " + hm1.get(null));

        // ✅ null value is allowed
        hm1.put("unset", null);
        System.out.println("Null value for 'unset': " + hm1.get("unset"));
    }
}

Output

From map: {Kotlin=2, Java=1} Config: {env=production, port=9090, host=localhost} Null key value: 99 Null value for 'unset': null

HashMap Methods — Complete Reference

HashMap implements the full Map interface including all Java 8+ default methods. None of the methods listed below are synchronized — HashMap is intended for single-threaded use.

MethodDescriptionReturns / Notes
put(K key, V value)Associates key with value. Overwrites if key already present. Null key and null value allowed.Previous value, or null if key was absent.
get(Object key)Returns value for the specified key.Value, or null if key not found (also null if key maps to null).
remove(Object key)Removes the entry for the specified key.Removed value, or null if key not present.
containsKey(Object key)Returns true if the map contains the specified key.boolean. O(1) average.
containsValue(Object value)Returns true if one or more keys map to the specified value.boolean. O(n) — scans all buckets.
size()Returns the total number of key-value entries.int
isEmpty()Returns true if the map contains no entries.boolean
clear()Removes all entries from the map.void
putAll(Map<? extends K,? extends V> m)Copies all entries from the given map.void
getOrDefault(Object key, V defaultValue)Returns value for key, or defaultValue if key is absent.V (Java 8+)
putIfAbsent(K key, V value)Inserts entry only if key is not already mapped to a non-null value.Existing value if present, null if newly inserted (Java 8+).
replace(K key, V value)Replaces value only if key currently maps to any value.Previous value or null (Java 8+).
replace(K key, V oldValue, V newValue)Replaces only if key maps to oldValue.boolean (Java 8+).
remove(Object key, Object value)Removes only if key maps to the specified value.boolean (Java 8+).
keySet()Returns a Set view of all keys.Set<K> — backed by the HashMap.
values()Returns a Collection view of all values.Collection<V> — backed by the HashMap.
entrySet()Returns a Set view of all Map.Entry pairs.Set<Map.Entry<K,V>> — backed by the HashMap.
forEach(BiConsumer)Performs the given action for each entry (Java 8+).void — cleanest iteration approach.
compute(K key, BiFunction)Computes new value for key; removes entry if function returns null (Java 8+).New computed value.
computeIfAbsent(K key, Function)Computes and inserts value only if key is absent or maps to null (Java 8+).Existing or computed value.
computeIfPresent(K key, BiFunction)Computes and replaces value only if key is present with non-null value (Java 8+).New value or null.
merge(K key, V value, BiFunction)Merges new value with existing value using function; removes if result is null (Java 8+).Merged value.
replaceAll(BiFunction)Replaces each value with the result of the function applied to each entry (Java 8+).void.
clone()Returns a shallow copy of the HashMap.Object — same entries, different HashMap instance.
toString()Returns a string: {key1=value1, key2=value2, ...}String
☕ JavaHashMap Methods — Hands-On Example
import java.util.*;

public class HashMapMethods {
    public static void main(String[] args) {

        HashMap<String, Integer> population = new HashMap<>();

        // put() — add entries
        population.put("Mumbai",    20_667_000);
        population.put("Delhi",     32_941_000);
        population.put("Bangalore", 13_193_000);
        population.put("Chennai",    7_088_000);
        System.out.println("Cities: " + population);

        // get()
        System.out.println("Delhi population: " + population.get("Delhi"));

        // containsKey() and containsValue()
        System.out.println("Has Mumbai: " + population.containsKey("Mumbai"));
        System.out.println("Pop 7088000 exists: " + population.containsValue(7_088_000));

        // getOrDefault() — Java 8
        System.out.println("Pune: " + population.getOrDefault("Pune", -1));

        // putIfAbsent() — Java 8
        population.putIfAbsent("Delhi", 0);      // Delhi exists — no change
        population.putIfAbsent("Pune", 7_764_000); // Pune is new — inserted
        System.out.println("After putIfAbsent: Delhi=" + population.get("Delhi")
                            + ", Pune=" + population.get("Pune"));

        // replace()
        population.replace("Chennai", 7_200_000);
        System.out.println("Updated Chennai: " + population.get("Chennai"));

        // compute() — Java 8: add 500_000 to Bangalore's population
        population.compute("Bangalore", (city, pop) -> pop + 500_000);
        System.out.println("Computed Bangalore: " + population.get("Bangalore"));

        // merge() — Java 8: useful for accumulating values
        population.merge("Mumbai", 1_000_000, Integer::sum);
        System.out.println("Merged Mumbai: " + population.get("Mumbai"));

        // remove()
        population.remove("Pune");
        System.out.println("After remove Pune: " + population.containsKey("Pune"));

        // forEach — Java 8
        System.out.println("--- All Cities ---");
        population.forEach((city, pop) ->
            System.out.printf("%-12s → %,d%n", city, pop));
    }
}

Output

Cities: {Chennai=7088000, Bangalore=13193000, Mumbai=20667000, Delhi=32941000} Delhi population: 32941000 Has Mumbai: true Pop 7088000 exists: true Pune: -1 After putIfAbsent: Delhi=32941000, Pune=7764000 Updated Chennai: 7200000 Computed Bangalore: 13693000 Merged Mumbai: 21667000 After remove Pune: false --- All Cities --- Chennai → 7,200,000 Bangalore → 13,693,000 Mumbai → 21,667,000 Delhi → 32,941,000

Load Factor and Rehashing in HashMap

Two concepts directly govern HashMap's performance characteristics: load factor and rehashing. Getting these right — especially for known-size use cases — can dramatically reduce memory allocations and GC pressure in production applications.

  • 📐 Load Factor — A float between 0.0 and 1.0 that determines the fill-ratio at which the bucket array is resized. Default is 0.75f. Formula: threshold = capacity × loadFactor. With defaults: threshold = 16 × 0.75 = 12. The 13th insertion triggers rehashing.

  • 🔄 Rehashing (resize) — When size exceeds threshold, HashMap creates a new bucket array exactly 2× the current capacity (e.g., 16 → 32 → 64 → …) — always a power of two. Every existing entry is re-inserted into the new array using the new index calculation. Rehashing is O(n). Unlike Hashtable, HashMap rehashing is single-threaded (no lock) and the new capacity formula is simply oldCapacity << 1.

  • 📉 Low Load Factor (e.g., 0.25) — Table stays sparse → very few collisions → fast lookups. Trade-off: higher memory usage and more frequent rehashing. Use when access speed is critical and memory is plentiful.

  • 📈 High Load Factor (e.g., 0.90) — Table stays dense → memory-efficient. Trade-off: more collisions → longer chains → slower lookups. Treeification kicks in for the worst buckets, but overall throughput still degrades.

  • ✅ Pre-sizing Best Practice — If the final map size is known, construct with: new HashMap<>(expectedSize / 0.75 + 1). This sets the initial capacity high enough that no rehashing occurs, avoiding the O(n) resize cost entirely. Example: for 100 entries → new HashMap<>(135).

☕ JavaLoad Factor Tuning and Pre-Sizing Example
import java.util.*;

public class HashMapLoadFactor {
    public static void main(String[] args) {

        // Strategy 1: Default — rehashing may occur
        HashMap<String, String> defaultMap = new HashMap<>();
        // Threshold = 16 × 0.75 = 12 → rehashes at 13th entry

        // Strategy 2: Pre-sized — no rehashing for up to 1000 entries
        // initialCapacity = 1000 / 0.75 + 1 = 1335 (rounded to next power of two: 2048)
        HashMap<String, String> preSeized = new HashMap<>(1335);

        // Strategy 3: High-speed cache — sparse table, fewer collisions
        HashMap<String, String> fastCache = new HashMap<>(256, 0.5f);

        // Populate pre-sized map
        for (int i = 1; i <= 1000; i++) {
            preSeized.put("key-" + i, "value-" + i);
        }
        System.out.println("Pre-sized entries: " + preSeized.size());
        System.out.println("No rehashing occurred — table pre-sized correctly.");
    }
}

Output

Pre-sized entries: 1000 No rehashing occurred — table pre-sized correctly.

HashMap vs Hashtable vs LinkedHashMap vs TreeMap

Choosing the right Map implementation is one of the most impactful design decisions in Java. Here is a complete comparison to help you choose correctly every time.

FeatureHashMapHashtableLinkedHashMapTreeMap
Introduced inJava 1.2Java 1.0 (legacy)Java 1.2Java 1.2
Thread-Safe?❌ No✅ Yes (all methods)❌ No❌ No
Null Key?✅ One null key❌ NullPointerException✅ One null key❌ NullPointerException (comparator may allow)
Null Values?✅ Multiple❌ NullPointerException✅ Multiple✅ Multiple
Iteration Order❌ None (unpredictable)❌ None✅ Insertion order (or access order)✅ Sorted by key (natural/Comparator)
Underlying StructureArray of Node chains + Red-Black Trees (Java 8+)Array of Entry chainsHashMap + doubly-linked listRed-Black Tree
get / put PerformanceO(1) averageO(1) average (+ lock overhead)O(1) averageO(log n)
Initial Capacity16 (power of two)11 (prime)16 (power of two)N/A — tree grows dynamically
Default Load Factor0.750.750.75N/A
ExtendsAbstractMapDictionaryHashMapAbstractMap
Best Use CaseGeneral-purpose key-value storeLegacy code onlyLRU cache, ordered iterationSorted map, range queries
ConcurrentHashMap replacement?Use ConcurrentHashMap for multi-threadYes — use ConcurrentHashMapUse ConcurrentSkipListMap for ordered concurrentUse ConcurrentSkipListMap for sorted concurrent

Decision Guide: Default to HashMap for all single-threaded Map needs. Choose LinkedHashMap when iteration order matters (e.g., building an LRU cache). Choose TreeMap when keys need to be sorted or you need range operations like subMap(), headMap(), or tailMap(). For multi-threaded scenarios, use ConcurrentHashMap (unordered) or ConcurrentSkipListMap (sorted). Avoid Hashtable in all new code.

Null Key and Null Value Handling in HashMap

One of HashMap's most distinctive features is its deliberate support for null keys and null values. Understanding exactly how this works — and where it can cause subtle bugs — is important for every Java developer.

☕ JavaNull Key and Null Value Behavior — Full Demonstration
import java.util.*;

public class HashMapNullDemo {
    public static void main(String[] args) {

        HashMap<String, String> map = new HashMap<>();

        // ✅ One null key — always stored at bucket index 0
        map.put(null, "default config");
        System.out.println("null key → " + map.get(null));

        // Overwrite null key (same as any other key)
        map.put(null, "updated config");
        System.out.println("null key (overwritten) → " + map.get(null));

        // ✅ Multiple null values
        map.put("primaryColor",   null);
        map.put("secondaryColor", null);
        map.put("accentColor",    "#FF5733");
        System.out.println("primaryColor → " + map.get("primaryColor"));

        // ⚠️ TRAP: get() returns null for BOTH absent keys AND null-value keys
        // Use containsKey() to distinguish them:
        System.out.println("get missing key: " + map.get("missingKey"));    // null
        System.out.println("get null-value key: " + map.get("primaryColor")); // null
        // Both return null — but the meaning is different!

        // Correct approach: containsKey() to distinguish
        String key = "primaryColor";
        if (map.containsKey(key)) {
            System.out.println(key + " is set (value may be null): " + map.get(key));
        } else {
            System.out.println(key + " does not exist in map");
        }

        // getOrDefault() avoids null-value ambiguity for absent keys
        System.out.println("font: " + map.getOrDefault("font", "Arial"));
    }
}

Output

null key → default config null key (overwritten) → updated config primaryColor → null get missing key: null get null-value key: null primaryColor is set (value may be null): null font: Arial

The null ambiguity trap: get(key) returns null when (a) the key is absent, or (b) the key is present but mapped to a null value. These two cases are indistinguishable from the return value alone. Always use containsKey() when you need to tell them apart, or prefer getOrDefault() to guarantee a non-null fallback for missing keys.

Iterating Over a HashMap

HashMap supports multiple iteration styles. Always prefer entrySet() or forEach — they are both more efficient and more readable than keySet()-based iteration (which requires a get() lookup for each key).

☕ JavaAll Ways to Iterate a HashMap
import java.util.*;
import java.util.stream.*;

public class IterateHashMap {
    public static void main(String[] args) {

        HashMap<String, Double> gpa = new HashMap<>();
        gpa.put("Arjun",  8.9);
        gpa.put("Sneha",  9.4);
        gpa.put("Vikram", 7.8);
        gpa.put("Pooja",  9.1);

        // 1. entrySet() with for-each — RECOMMENDED: accesses both key and value
        System.out.println("--- entrySet ---");
        for (Map.Entry<String, Double> entry : gpa.entrySet()) {
            System.out.println(entry.getKey() + " → " + entry.getValue());
        }

        // 2. keySet() — only when keys alone are needed
        System.out.println("--- keySet ---");
        for (String name : gpa.keySet()) {
            System.out.println(name + " : " + gpa.get(name));
        }

        // 3. values() — iterate values only
        System.out.println("--- values ---");
        for (double score : gpa.values()) {
            System.out.println(score);
        }

        // 4. forEach with lambda (Java 8+) — cleanest
        System.out.println("--- forEach lambda ---");
        gpa.forEach((name, score) ->
            System.out.printf("%-8s GPA: %.1f%n", name, score));

        // 5. Stream API — filter and process
        System.out.println("--- Stream: GPA > 9.0 ---");
        gpa.entrySet().stream()
           .filter(e -> e.getValue() > 9.0)
           .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
           .forEach(e -> System.out.println(e.getKey() + " → " + e.getValue()));

        // ⚠️ HashMap's Iterator is FAIL-FAST.
        // Modifying the map during iteration throws ConcurrentModificationException.
        // Safe removal during iteration: use Iterator.remove()
        System.out.println("--- Safe removal during iteration ---");
        Iterator<Map.Entry<String, Double>> it = gpa.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Double> e = it.next();
            if (e.getValue() < 8.0) {
                it.remove();  // safe — removes from underlying map
                System.out.println("Removed: " + e.getKey());
            }
        }
        System.out.println("After removal: " + gpa);
    }
}

HashMap put() Operation Flow — Flowchart

The flowchart below shows the complete decision path Java follows when put(key, value) is called on a HashMap, including the Java 8 treeification step.

📝 put(key, value)called on HashMap
❓ key == null?
Yes — null key
📌 Store at index 0putForNullKey() — special path
🔢 Compute spread hashh = key.hashCode() ^ (h >>> 16)
📂 Find bucket indexindex = hash & (capacity - 1)
❓ Key exists in bucket?scan chain/tree with equals()
Yes — key found
♻️ Update existing valuereturn old value
➕ Append new Nodeto chain tail (or insert into tree)
❓ Chain length ≥ 8?TREEIFY_THRESHOLD exceeded?
Yes — chain is long
❓ Table capacity ≥ 64?MIN_TREEIFY_CAPACITY check
Yes — treeify
🌳 Treeify bucketLinked list → Red-Black Tree
📏 Resize table insteadcapacity too small — resize first
❓ size > threshold?capacity × loadFactor exceeded?
Yes — rehash
🔄 resize()capacity × 2 (always power of two)
✅ Donereturn null (new key inserted)

Code Execution Flow — from source to output

HashMap with Generics and Custom Objects

HashMap is fully generic and works with any object as key or value. When using a custom class as a key, you must correctly override both hashCode() and equals(). Failing to do so is one of the most common — and hardest-to-debug — bugs in Java Map usage.

☕ JavaCustom Key in HashMap — hashCode + equals Contract
import java.util.*;

class EmployeeId {
    private final String department;
    private final int    empNumber;

    EmployeeId(String department, int empNumber) {
        this.department = department;
        this.empNumber  = empNumber;
    }

    // MUST override equals — HashMap uses this to find keys in bucket chains
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof EmployeeId)) return false;
        EmployeeId other = (EmployeeId) o;
        return empNumber == other.empNumber
               && department.equals(other.department);
    }

    // MUST override hashCode — determines which bucket the key lands in
    // Contract: if a.equals(b) then a.hashCode() == b.hashCode()
    @Override
    public int hashCode() {
        return Objects.hash(department, empNumber);
    }

    @Override
    public String toString() {
        return department + "-" + empNumber;
    }
}

public class HashMapCustomKey {
    public static void main(String[] args) {

        HashMap<EmployeeId, String> directory = new HashMap<>();
        directory.put(new EmployeeId("TECH", 1001), "Ananya Sharma");
        directory.put(new EmployeeId("TECH", 1002), "Rohan Mehta");
        directory.put(new EmployeeId("HR",   2001), "Kavya Reddy");

        // Lookup with a NEW EmployeeId object — same field values
        // Succeeds ONLY because hashCode() and equals() are correctly overridden
        String name = directory.get(new EmployeeId("TECH", 1001));
        System.out.println("Found: " + name);

        // Without overriding hashCode/equals, this would return null
        // because two different object instances would land in different buckets

        directory.forEach((id, empName) ->
            System.out.println(id + " => " + empName));
    }
}

Output

Found: Ananya Sharma HR-2001 => Kavya Reddy TECH-1002 => Rohan Mehta TECH-1001 => Ananya Sharma

Best Practices for Using HashMap

Following these practices ensures that your HashMap usage is correct, performant, and maintainable — whether in single-threaded utilities or complex enterprise applications.

  • ✅ 1. Declare as Map Interface Reference — Always declare variables as Map<K,V> rather than HashMap<K,V>. This decouples your code from the implementation and allows swapping to LinkedHashMap, TreeMap, or ConcurrentHashMap with a single line change: Map<String, Integer> scores = new HashMap<>();

  • ✅ 2. Pre-Size for Known Workloads — If the approximate number of entries is known, use: new HashMap<>(expectedSize / 0.75 + 1). This prevents rehashing entirely, saving O(n) resize operations and reducing GC pressure. For 1000 expected entries: new HashMap<>(1335).

  • ✅ 3. Always Override hashCode() and equals() for Custom Keys — Both methods must be consistent: if a.equals(b) is true, then a.hashCode() must equal b.hashCode(). Using IDE-generated or Objects.hash() implementations is recommended. Never use mutable fields in hashCode() computation for keys — changing a key's state after insertion makes it unretrievable.

  • ✅ 4. Use getOrDefault() Instead of get() + null Check — Prefer map.getOrDefault(key, defaultValue) over if (map.get(key) != null). The former is cleaner and handles the null-value ambiguity case correctly. For maps that may hold null values, use containsKey() to check existence.

  • ✅ 5. Use computeIfAbsent() for Lazy Initialization — Instead of checking containsKey() then calling put(), use computeIfAbsent(key, k -> new ArrayList<>()). It is atomic enough for single-threaded code and expresses intent more clearly. Example: map.computeIfAbsent("group", k -> new ArrayList<>()).add(item);

  • ✅ 6. Iterate with entrySet() or forEach — Never iterate with keySet() and call get() for each key — that's two lookups per entry. Use entrySet() or forEach(BiConsumer) which provides direct access to both key and value in a single traversal.

  • ❌ 7. Do Not Use HashMap in Multi-Threaded Code Without Synchronization — HashMap is not thread-safe. Concurrent modifications can cause infinite loops during rehash, data loss, or ConcurrentModificationException. Use ConcurrentHashMap for shared state between threads — never Collections.synchronizedMap(new HashMap<>()) which suffers the same coarse-locking problem as Hashtable.

  • ❌ 8. Do Not Use Mutable Objects as HashMap Keys — If a key object's fields change after insertion, its hashCode() changes, placing it in a different bucket. The original entry becomes permanently unreachable (memory leak). Always use immutable objects — String, Integer, enums, or custom immutable classes — as HashMap keys.

Real-World Code Examples

Example 1 — Word Frequency Counter

☕ JavaWord Frequency Counter Using HashMap
import java.util.*;
import java.util.stream.*;

public class WordFrequency {
    public static void main(String[] args) {

        String text = "the quick brown fox jumps over the lazy dog " +
                      "the dog barked at the fox and the fox ran away";

        HashMap<String, Integer> freq = new HashMap<>();

        // Count frequencies using merge() — cleanest approach
        for (String word : text.split("\\s+")) {
            freq.merge(word, 1, Integer::sum);
        }

        System.out.println("All word frequencies:");
        freq.forEach((word, count) ->
            System.out.printf("  %-10s : %d%n", word, count));

        // Top 3 most frequent words using Stream API
        System.out.println("\nTop 3 words:");
        freq.entrySet().stream()
           .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
           .limit(3)
           .forEach(e ->
               System.out.printf("  %-10s : %d times%n", e.getKey(), e.getValue()));
    }
}

Output

All word frequencies: the : 5 fox : 3 dog : 2 quick : 1 brown : 1 jumps : 1 over : 1 lazy : 1 barked : 1 at : 1 and : 1 ran : 1 away : 1 Top 3 words: the : 5 times fox : 3 times dog : 2 times

Example 2 — Grouping Data with computeIfAbsent()

☕ JavaGrouping Students by Department Using HashMap
import java.util.*;

public class GroupByDepartment {

    record Student(String name, String dept, double gpa) {}

    public static void main(String[] args) {

        List<Student> students = List.of(
            new Student("Arjun",   "CS",      8.9),
            new Student("Sneha",   "ECE",     9.2),
            new Student("Vikram",  "CS",      7.8),
            new Student("Pooja",   "MECH",    8.5),
            new Student("Rahul",   "ECE",     8.1),
            new Student("Priya",   "CS",      9.5),
            new Student("Aditya",  "MECH",    7.3)
        );

        // Group students by department — computeIfAbsent avoids null check + put
        HashMap<String, List<String>> byDept = new HashMap<>();
        for (Student s : students) {
            byDept.computeIfAbsent(s.dept(), k -> new ArrayList<>())
                  .add(s.name() + " (" + s.gpa() + ")");
        }

        // Print grouped results
        System.out.println("Students by Department:");
        byDept.entrySet().stream()
              .sorted(Map.Entry.comparingByKey())
              .forEach(e -> {
                  System.out.println("  " + e.getKey() + ":");
                  e.getValue().forEach(s -> System.out.println("    → " + s));
              });

        // Average GPA per department using merge()
        HashMap<String, Double> totalGpa   = new HashMap<>();
        HashMap<String, Integer> deptCount = new HashMap<>();
        for (Student s : students) {
            totalGpa.merge(s.dept(),   s.gpa(), Double::sum);
            deptCount.merge(s.dept(),  1,       Integer::sum);
        }
        System.out.println("\nAverage GPA by Department:");
        totalGpa.forEach((dept, total) ->
            System.out.printf("  %-6s : %.2f%n", dept, total / deptCount.get(dept)));
    }
}

Output

Students by Department: CS: → Arjun (8.9) → Vikram (7.8) → Priya (9.5) ECE: → Sneha (9.2) → Rahul (8.1) MECH: → Pooja (8.5) → Aditya (7.3) Average GPA by Department: CS : 8.73 ECE : 8.65 MECH : 7.90

Practice This Code — Live Editor

Advantages and Disadvantages of HashMap

HashMap is the workhorse of Java's Collections Framework — flexible, fast, and feature-rich. But like every data structure, it has specific trade-offs you must understand to use it correctly.

✅ Advantages
O(1) Average-Case Performanceget() and put() both run in O(1) average time due to direct bucket-index computation. In the rare worst case (all keys collide into one bucket), performance degrades to O(n) for a plain linked-list chain — but Java 8's treeification caps this at O(log n) for large collision chains.
Null Key and Null Value SupportUnlike Hashtable and ConcurrentHashMap, HashMap accepts one null key and any number of null values. This flexibility is valuable when null is a meaningful sentinel in your data model — e.g., representing 'not yet assigned' or 'config not set'.
Rich Java 8+ APIcompute(), computeIfAbsent(), computeIfPresent(), merge(), replaceAll(), and forEach() make HashMap a powerful tool for functional-style data transformation without needing to manually iterate and conditionally update entries.
Java 8 TreeificationThe automatic conversion of long collision chains to Red-Black Trees (when chain length > 8 AND table capacity >= 64) protects against hash-flooding attacks and adversarial inputs. Worst-case lookup is O(log n) instead of O(n).
No Synchronization OverheadHashMap has zero locking overhead in single-threaded code. Unlike Hashtable — which acquires and releases a monitor lock on every method call — HashMap performs pure computation, making it measurably faster in profiling benchmarks for single-threaded workloads.
Memory-Efficient Power-of-Two ResizingThe bucket index is computed with bitwise AND (hash & (capacity-1)), which is faster than modulo. The capacity always doubles during resize, minimizing the number of resize events. Hash spreading (XOR with right-shifted bits) compensates for poor hashCode distributions.
❌ Disadvantages
Not Thread-SafeConcurrent modification by multiple threads without external synchronization can cause data corruption, lost updates, or even infinite loops during rehashing. This is the most critical limitation — never share a HashMap across threads without ConcurrentHashMap or explicit synchronization.
No Guaranteed Iteration OrderHashMap provides no ordering guarantees. Iteration order appears random, can vary across JVM implementations, and can change after a rehash. If predictable order is needed, LinkedHashMap (insertion order) or TreeMap (sorted order) must be used instead.
null Ambiguity on get()get(key) returns null both when the key is absent and when the key maps to a null value. This ambiguity requires extra containsKey() calls to distinguish, adding verbosity and a potential source of bugs for developers who forget this behavior.
Rehashing Cost and GC PressureWhen the load factor threshold is exceeded, rehashing creates a new backing array 2× the size and re-inserts all entries. For large maps, this is O(n) in both time and memory — the old and new arrays coexist temporarily, doubling memory usage at peak. Pre-sizing eliminates this risk.
Broken Keys from Mutable ObjectsIf a key object's state changes after insertion (altering its hashCode()), the entry becomes permanently unreachable — it sits in the wrong bucket, creating a memory leak and a missing entry. Always use immutable key objects (String, Integer, enums, or custom @Immutable classes).
hashCode() Quality DependencyHashMap's performance is entirely dependent on the quality of keys' hashCode() implementations. A poor hashCode() that returns the same value for all keys collapses the HashMap into a single linked list (O(n) lookups), negating all benefits. Well-distributed hash codes are essential.

Java HashMap — Interview Questions

These are the most frequently asked interview questions on Java HashMap in Java developer interviews — from junior to senior level. HashMap internals are among the most tested topics in Java collections interviews.

Practice Questions — Test Your Knowledge

Test your understanding of HashMap with these practice questions covering output prediction, design decisions, and internal behavior.

1. What will the following code output? HashMap<String,Integer> hm = new HashMap<>(); hm.put("A", 10); hm.put("B", 20); hm.put("A", 30); System.out.println(hm.size() + " " + hm.get("A"));

Easy

2. What is the output? HashMap<String,String> hm = new HashMap<>(); hm.put(null, "zero"); hm.put(null, "updated"); System.out.println(hm.size() + " " + hm.get(null));

Easy

3. You need to count character frequencies in a String using HashMap. Write the most concise correct approach using Java 8 methods.

Easy

4. A HashMap has initialCapacity=16, loadFactor=0.75. After how many put() calls does the first rehash occur? What is the new capacity?

Medium

5. What does the following output? Is there a bug? HashMap<String,String> hm = new HashMap<>(); hm.put("color", null); if (hm.get("color") == null) { System.out.println("color not found"); } else { System.out.println("color = " + hm.get("color")); }

Medium

6. Two HashMap keys have hashCode() returning the same value but equals() returning false. What happens when both are put into a HashMap?

Medium

7. Explain why the following code is NOT thread-safe even when using ConcurrentHashMap: if (!map.containsKey(key)) { map.put(key, value); }

Hard

8. A custom class overrides hashCode() to always return 42 for all instances and equals() correctly. What is the performance impact on a HashMap with 1000 such keys?

Hard

Conclusion — HashMap as the Foundation of Java Data Storage

HashMap is the backbone of key-value storage in Java. From simple configuration maps to complex in-memory caches and data aggregation pipelines, virtually every non-trivial Java application uses HashMap directly or through frameworks built on top of it. Its O(1) average-case performance, rich Java 8+ API, and null-key flexibility make it the default choice for the vast majority of Map use cases.

Understanding its internals — the power-of-two bucket array, hash spreading, separate chaining, treeification, load factor, and rehashing — is not just academic knowledge. It directly informs decisions about pre-sizing, key design (immutability and hashCode quality), and when to reach for LinkedHashMap, TreeMap, or ConcurrentHashMap instead.

ScenarioRecommendation
General-purpose key-value store (single-threaded)✅ Use HashMap — fastest, most flexible, modern API
Need insertion-order iteration✅ Use LinkedHashMap — extends HashMap, adds ordering
Need sorted key order or range queries✅ Use TreeMap — O(log n) but always sorted
Multi-threaded concurrent access✅ Use ConcurrentHashMap — fine-grained locking, atomic ops
Multi-threaded sorted map✅ Use ConcurrentSkipListMap
Legacy code that uses Hashtable⚠️ Migrate to HashMap or ConcurrentHashMap gradually
Keys might be mutable objects❌ Redesign — always use immutable keys in HashMap

Your next steps: study LinkedHashMap (insertion-order map and LRU cache patterns), TreeMap (Red-Black Tree sorted map and NavigableMap API), and ConcurrentHashMap (segment-level locking and atomic compound operations). Together these form the complete picture of Java's key-value storage ecosystem. ☕

Frequently Asked Questions (FAQ)