JavaCollectionsintermediate
Updated:

HashMap in Java: Operations, Iteration and Best Practices

5 min read

A practical guide to HashMap in Java — every core operation, all the iteration patterns, and the best practices that keep production code fast and bug-free.

TL;DR – Quick Answer

HashMap is Java's most-used Map implementation. It stores key-value pairs in hash buckets, giving average O(1) put, get and remove. It allows one null key, is not thread-safe, and guarantees no iteration order. Use entrySet() or forEach() to iterate, and Iterator.remove() when deleting entries during iteration.

On This Page

Open any Java codebase and count the collection types. ArrayList wins, and HashMap is right behind it. Caching, counting, grouping, indexing, de-duplicating — HashMap is the workhorse for all of them because it turns "find X" from a scan into a single hash lookup.

This guide covers the operations you will use every day, every iteration pattern with its trade-offs, and the best practices that separate working code from production-quality code. For the internals — buckets, treeification, resizing — see the companion deep dive on how HashMap works internally.

What HashMap gives you

HashMap<K, V> implements the Map interface with a hash table. Its contract in five points:

  • Average O(1) for put, get, remove and containsKey
  • No ordering guarantee — iteration order can change as the map grows
  • One null key and unlimited null values are allowed
  • Not thread-safe — see HashMap vs ConcurrentHashMap for the concurrent story
  • Fail-fast iterators — structural modification during iteration throws ConcurrentModificationException
Operation Average Worst case (Java 8+)
put(k, v) O(1) O(log n)
get(k) O(1) O(log n)
remove(k) O(1) O(log n)
containsKey(k) O(1) O(log n)
containsValue(v) O(n) O(n)
iteration O(capacity + size) O(capacity + size)

The worst case is O(log n), not O(n), because Java 8 converts long collision chains into balanced trees. That single fact answers a very common interview follow-up.

Core operations with a runnable example

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

public class HashMapOperations {
    public static void main(String[] args) {
        Map<String, Double> prices = new HashMap<>();

        // Create / update
        prices.put("idli", 40.0);
        prices.put("dosa", 60.0);
        Double old = prices.put("dosa", 65.0);   // returns 60.0
        prices.putIfAbsent("dosa", 99.0);        // no effect, key exists

        // Read
        System.out.println(prices.get("dosa"));            // 65.0
        System.out.println(prices.get("vada"));            // null
        System.out.println(prices.getOrDefault("vada", 0.0)); // 0.0

        // Delete
        prices.remove("idli");
        prices.remove("dosa", 100.0);  // conditional: value mismatch, no-op

        // Inspect
        System.out.println(prices.containsKey("dosa"));  // true
        System.out.println(prices.size());               // 1
        System.out.println(old);                         // 60.0
    }
}

Three details worth noticing: put() returns the previous value (or null), putIfAbsent() only writes when the key is missing, and the two-argument remove(key, value) deletes only when the current value matches — handy for optimistic updates.

Common mistake: A common mistake beginners make is treating null from get() as "key absent". It can also mean "key present, value is null". When that distinction matters, check containsKey() — or better, never store null values in the first place.

The Java 8 methods you should be using

getOrDefault, computeIfAbsent, compute and merge replace almost every check-then-put pattern:

import java.util.*;

public class ModernHashMap {
    public static void main(String[] args) {
        String[] orders = {"veg", "chicken", "veg", "paneer", "veg"};

        // Counting: one line instead of if-contains-else
        Map<String, Integer> count = new HashMap<>();
        for (String o : orders) {
            count.merge(o, 1, Integer::sum);
        }
        System.out.println(count); // {chicken=1, veg=3, paneer=1}

        // Grouping: computeIfAbsent creates the list only once
        Map<Character, List<String>> byInitial = new HashMap<>();
        for (String o : orders) {
            byInitial.computeIfAbsent(o.charAt(0), k -> new ArrayList<>())
                     .add(o);
        }
        System.out.println(byInitial);
        // {p=[paneer], c=[chicken], v=[veg, veg, veg]}

        // Transform a value in place
        count.compute("veg", (k, v) -> v == null ? 1 : v * 10);
        System.out.println(count.get("veg")); // 30
    }
}

The counting and grouping idioms above appear in a huge share of coding-round problems — two-sum, anagram grouping, frequency sorting. If you can write merge(word, 1, Integer::sum) without thinking, you have a real speed advantage in a timed collections interview round.

Iterating a HashMap: four patterns

1. entrySet() — the default

for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + " = " + e.getValue());
}

One pass, key and value together. Use this unless you have a reason not to.

2. forEach with a lambda

map.forEach((key, value) -> System.out.println(key + " = " + value));

Same performance, less ceremony. Best for short loop bodies.

3. keySet() or values() — when you need only one side

for (String key : map.keySet()) { /* keys only */ }
for (Integer val : map.values()) { /* values only */ }

Fine on their own. The anti-pattern is looping keySet() and calling map.get(key) inside — that hashes every key a second time for nothing.

4. Iterator — when you must remove while iterating

Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
    if (it.next().getValue() == 0) {
        it.remove();   // safe structural removal
    }
}
// Java 8 alternative:
map.entrySet().removeIf(e -> e.getValue() == 0);

Common mistake: A common mistake beginners make is calling map.remove(key) inside a for-each loop. The iterator's modification count no longer matches the map's, and the very next iteration throws ConcurrentModificationException. Use Iterator.remove() or removeIf() instead.

Capacity, load factor and performance

A HashMap starts with 16 buckets and resizes — doubling the table and redistributing every entry — once size exceeds capacity × load factor (0.75 by default, so 12 entries). Resizing is O(n), and repeated resizes while loading a large dataset add up.

If you know the target size, pre-size the map:

// Will hold ~10,000 entries: avoid ~10 resize passes
Map<String, User> users = new HashMap<>(16384);
// Or let Java 19+ compute capacity from expected size:
// Map<String, User> users = HashMap.newHashMap(10000);

Choosing 16384 (the next power of two above 10000 / 0.75) means the map never resizes during the load. The mechanics of why capacity is always a power of two are covered in the internal working deep dive.

Pro tip: Don't tune load factor. Lowering it wastes memory for a marginal speedup; raising it increases collisions. In ten years of production Java, the default 0.75 is almost always the right answer — pre-sizing capacity is the optimization that actually matters.

Best practices checklist

Use immutable keys. String, Integer, enums and records are safe. If a key object's hashCode() changes after insertion, the entry is stranded in the wrong bucket and effectively lost.

Override hashCode() and equals() together. If your custom class is a key, both methods must agree: equal objects must produce equal hash codes. A record gives you both for free:

record EmployeeId(String dept, int number) {}  // hashCode + equals generated

Map<EmployeeId, String> names = new HashMap<>();
names.put(new EmployeeId("ENG", 42), "Priya");
System.out.println(names.get(new EmployeeId("ENG", 42))); // Priya

Program to the interface. Declare Map<String, Integer>, not HashMap<String, Integer>, so you can swap implementations without touching call sites.

Pick the right sibling when order matters. LinkedHashMap for insertion order, TreeMap for sorted keys. A HashSet is literally a HashMap with dummy values, so the same reasoning applies to HashSet.

Never share a plain HashMap across writing threads. Lost updates and corrupted state are the symptoms; ConcurrentHashMap is the cure. The legacy Hashtable also "works" but at a heavy cost — the comparison with Hashtable explains why it lost.

Interview note: Interviewers often chain questions: "What's the complexity of get()?" then "When is it not O(1)?" then "What did Java 8 change about that?" The chain ends at treeification and the hashCode/equals contract. Prepare the whole chain, not just the first link.

Where to go next

You now have the practical layer: operations, iteration, capacity tuning and the key contracts. Two follow-ups complete the picture. First, the internal mechanics of HashMap — the most reliably asked deep-dive topic in Java interviews at every experience level. Second, the thread-safety story in HashMap vs ConcurrentHashMap, which matters the moment your code runs on more than one thread.

Frequently Asked Questions

What is the time complexity of HashMap operations?
put(), get(), remove() and containsKey() run in O(1) on average. In the worst case, when many keys collide into one bucket, operations degrade to O(log n) in Java 8+ because long collision chains convert to balanced trees. containsValue() is always O(n) since it scans every entry.
How do I iterate over a HashMap in Java?
The standard way is a for-each loop over entrySet(), which gives you key and value together in one pass. Java 8 added map.forEach((k, v) -> ...) which is more concise. Iterate keySet() only when you need keys alone, and values() when you need values alone.
Can I modify a HashMap while iterating over it?
Not with put() or remove() directly — the iterator detects the structural change and throws ConcurrentModificationException on the next step. Use Iterator.remove() to delete safely during iteration, or entry.setValue() to update values, or collect keys first and modify after the loop.
Is HashMap thread-safe in Java?
No. Concurrent writes from multiple threads can lose updates or corrupt internal state. For concurrent access use ConcurrentHashMap, which locks at a fine-grained level and stays fast under contention. Collections.synchronizedMap() works but serializes every operation.
Why is HashMap iteration order not guaranteed?
Entries are placed in buckets by hash code, and the bucket index changes when the table resizes. So iteration order depends on capacity and insertion history, and can change as the map grows. If you need insertion order use LinkedHashMap; for sorted keys use TreeMap.
What is the initial capacity of a HashMap?
The default initial capacity is 16 buckets, with a load factor of 0.75, meaning the table doubles once it holds more than 12 entries. If you know you will store thousands of entries, pass an initial capacity to the constructor to avoid repeated resize passes.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 14 July 2026 LinkedIn
Chat with us