Iterate HashMap in Java: Fastest Way With JMH Results

Learn to iterate HashMap in Java with entrySet, forEach, keySet and streams, remove entries safely, and compare each way with JMH results on JDK 25.

Bar chart of JMH average time per HashMap iteration on JDK 25 for entrySet, forEach, Iterator, values, stream and keySet plus get, at 100 and 100,000 entries

The best way to iterate HashMap entries is a for-each loop over entrySet() or the Map.forEach() method, because both read every key and value in one pass without looking up any key again. Looping over keySet() and calling get() for each key does an extra hash lookup per entry, which makes it the slowest of the common ways.

We iterate a map whenever we need all its entries, for example to export stock levels to a CSV file, to total the values, or to remove entries that match a condition. The following example walks the same small stock map in each common way, and every loop computes the total units.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2, "pot", 3));
int a = 0;
for (Map.Entry<String, Integer> e : stock.entrySet()) { a += e.getValue(); }
int viaEntrySet = a;                                                      // 10
int[] b = {0};
stock.forEach((sku, units) -> b[0] += units);
int viaForEach = b[0];                                                    // 10
int viaValues = stock.values().stream().mapToInt(Integer::intValue).sum(); // 10
int c = 0;
for (String sku : stock.keySet()) { c += stock.get(sku); }
int viaKeySet = c;                                                        // 10, one extra get() per key

Notice that all four loops give the same total, and the order in which they visit the keys is not defined for a HashMap. We compare the ways in a table, remove and update entries safely during a loop, control the order, and look at a JMH run on JDK 25 that shows which differences are real.

1. Ways to Iterate HashMap Entries

A HashMap is not a Collection, so a for-each loop cannot take the map itself. We loop over one of its three views, namely entrySet(), keySet() or values(), or we pass a lambda to forEach(). The views are live, so they reflect the map and do not copy it.

WayGives usRemove during the loopExtra lookup
for-each over entrySet()Key and valueNo (use an Iterator)No
Map.forEach((k, v) -> …)Key and valueNoNo
for-each over values()Value onlyNoNo
for-each over keySet() + get()Key, value through get()NoYes, one get() per key
Iterator over entrySet()Key and valueYes, iterator.remove()No
entrySet().stream()Key and value in a pipelineNo (collect a new map)No

1.1. Looping Over entrySet()

The entrySet() view returns each mapping as a Map.Entry, so getKey() and getValue() read the fields of the internal node without any hashing. It is the default choice when the loop body needs both the key and the value, or uses break or a checked exception.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 0));
List<String> soldOut = new ArrayList<>();
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
    if (entry.getValue() == 0) {
        soldOut.add(entry.getKey());
    }
}
List<String> result = soldOut;                     // [mug]

With var the loop header gets shorter, and since Java 22 we can name an unused lambda parameter _ when we only need one side of the entry.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2));
int units = 0;
for (var entry : stock.entrySet()) {
    units += entry.getValue();
}
int total = units;                                 // 7
int[] sum = {0};
stock.forEach((_, u) -> sum[0] += u);
int viaUnnamed = sum[0];                           // 7

1.2. Using Map.forEach()

The Map.forEach() method, added in Java 8, takes a BiConsumer that gets the key and the value. HashMap overrides it and walks its own bucket array, so it creates no iterator object. It is the shortest form for a loop body that is one statement.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2));
StringBuilder csv = new StringBuilder();
stock.forEach((sku, units) -> csv.append(sku).append(',').append(units).append('\n'));
int lines = csv.toString().lines().toList().size();         // 2

A lambda cannot change a local variable or throw a checked exception, so a body that needs either one is clearer as an entrySet() loop. A return inside the lambda only skips the current entry and does not stop the loop.

1.3. Looping Over keySet() or values()

When we need only the keys or only the values, the keySet() and values() views skip the part we do not use. The problem starts when a keySet() loop calls get() to read the value, because get() computes the hash of the key again and searches its bucket, which repeats work the iterator already did.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2));
Set<String> skus = new TreeSet<>(stock.keySet());          // [mug, tea]
int units = 0;
for (int u : stock.values()) {
    units += u;
}
int total = units;                                         // 7

1.4. Using an Iterator

An explicit Iterator is what the for-each loop uses behind the scenes, so it is not faster. We write it out only when we need iterator.remove(), which deletes the current entry without breaking the loop, as shown in section 2.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 0, "pot", 0));
Iterator<Map.Entry<String, Integer>> it = stock.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, Integer> entry = it.next();
    if (entry.getValue() == 0) {
        it.remove();
    }
}
int left = stock.size();                                   // 1

1.5. Streaming the Entries

A stream over entrySet() fits when the loop filters, transforms or collects into a new structure. For a plain loop it adds the cost of the pipeline objects, which is noticeable on small maps, as the measurements in section 4 show.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2, "pot", 0));
List<String> lowStock = stock.entrySet().stream()
        .filter(e -> e.getValue() < 3)
        .map(Map.Entry::getKey)
        .sorted()
        .toList();
List<String> result = lowStock;                            // [mug, pot]

Parallel streams split the bucket array between threads, which pays off only for large maps with an expensive body. The Java Streams guide covers the stream operations used here.

2. Removing and Updating Entries During Iteration

The iterators of HashMap are fail-fast. If we call map.remove() or map.put() with a new key inside a for-each loop or a forEach() call on a view, the next call to the iterator throws ConcurrentModificationException. Changing the value of an existing key is not a structural change, so it is allowed.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 0, "pot", 3));
stock.keySet().forEach(sku -> { if (stock.get(sku) == 0) stock.remove(sku); });   // ConcurrentModificationException

To remove entries while iterating, we call removeIf() on a view, or remove() on the iterator, never remove() on the map. The removeIf() method, added in Java 8, is the shortest safe form, and it works on entrySet(), keySet() and values().

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 0, "pot", 0));
boolean removed = stock.values().removeIf(units -> units == 0);      // true
int left = stock.size();                                             // 1
Map<String, Integer> prices = new HashMap<>(Map.of("tea", 450, "mug", 1200));
prices.replaceAll((sku, cents) -> cents + 50);
Integer mug = prices.get("mug");                                     // 1250

To update values in a loop that also reads the key, we call setValue() on the entry. It writes the value into the map, and the iterator keeps working.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2));
for (Map.Entry<String, Integer> entry : stock.entrySet()) {
    entry.setValue(entry.getValue() * 2);
}
Integer tea = stock.get("tea");                            // 10

When several threads change the map during the loop, none of these rules help, because HashMap is not thread-safe. A ConcurrentHashMap iterates without throwing ConcurrentModificationException, as explained in ConcurrentHashMap vs Collections.synchronizedMap().

3. Iteration Order of a HashMap

A HashMap visits entries in bucket order, which depends on the hash codes, the capacity and the insertion history. The order is not insertion order, and it can change after a resize or between Java versions, so code must never depend on it.

When the order matters, we pick a map that defines it, or we sort while iterating.

  • LinkedHashMap iterates in insertion order, and since Java 21 it also implements SequencedMap, so reversed(), firstEntry() and lastEntry() work on it.
  • TreeMap iterates in key order, or in the order of a Comparator.
  • A stream sorted with Map.Entry.comparingByKey() or comparingByValue() gives an order for one loop without changing the map.
Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 2, "pot", 3));
List<String> byKey = stock.entrySet().stream()
        .sorted(Map.Entry.comparingByKey())
        .map(e -> e.getKey() + "=" + e.getValue())
        .toList();                                         // [mug=2, pot=3, tea=5]
List<String> keys = byKey;                                 // [mug=2, pot=3, tea=5]
LinkedHashMap<String, Integer> ordered = new LinkedHashMap<>();
Integer p1 = ordered.put("tea", 5);                        // null
Integer p2 = ordered.put("mug", 2);                        // null
String last = ordered.reversed().firstEntry().getKey();    // "mug"

4. Measuring Iteration Performance on JDK 25

Iteration speed depends on how much work each way does per entry. The entrySet() loop, the values() loop and the explicit Iterator run the same internal HashIterator, which walks the bucket array and returns each node. HashMap.forEach() walks the same array in a plain loop. The keySet() + get() loop does that walk too and adds a hash lookup per key, and a stream adds pipeline objects on top of the walk.

We checked this reasoning with a JMH 1.35 benchmark on JDK 25.0.4, run on a cloud VM with 2 vCPUs, one fork, 5 warm-up and 5 measured iterations of one second each. The map held String keys such as “key42” and Integer values, and each benchmark summed the key lengths and the values. The numbers are average microseconds per full iteration, lower is better, and they come from this one run.

Way100 entries (us)100,000 entries (us)
for-each over values() (value only)0.31 +- 0.061,037 +- 349
for-each over entrySet()0.34 +- 0.031,017 +- 142
Map.forEach()0.35 +- 0.21974 +- 277
Iterator over entrySet()0.35 +- 0.061,027 +- 235
entrySet().stream() + mapToLong().sum()0.64 +- 0.041,346 +- 248
for-each over keySet() + get()0.75 +- 0.171,361 +- 177
Bar chart of JMH average time per HashMap iteration on JDK 25 for entrySet, forEach, Iterator, values, stream and keySet plus get, at 100 and 100,000 entries
entrySet(), forEach(), Iterator and values() tie within the error; keySet() + get() and streams cost more, most visibly on small maps

The first four ways tie within the error margins at both sizes, so the old advice that forEach() or the Iterator is slower does not hold on JDK 25. The keySet() + get() loop took about twice as long on 100 entries and about a third longer on 100,000, where cache misses on the scattered nodes dominate every way. The stream cost about twice as much on the small map, because building the pipeline costs as much as walking 100 entries.

Capacity matters as well. Iterating the views takes time proportional to the capacity plus the number of entries, so a map created with a huge initial capacity and few entries iterates slowly over empty buckets. We size maps for the expected entries, as explained in HashMap initial capacity and load factor, and we measure our own workload with JMH before we optimize a loop.

5. Exporting a Product Report From a HashMap

A warehouse app keeps the units of each product in a HashMap and exports a nightly report. The report lists products sorted by name, drops products that are out of stock, and flags the ones below a reorder level. One pass over entrySet() as a stream does all three, and the map itself stays unchanged for the next day.

record ReportLine(String sku, int units, boolean reorder) {}
Map<String, Integer> stock = new HashMap<>(Map.of("tea", 40, "mug", 3, "pot", 0, "cup", 12));
List<ReportLine> report = stock.entrySet().stream()
        .filter(e -> e.getValue() > 0)
        .sorted(Map.Entry.comparingByKey())
        .map(e -> new ReportLine(e.getKey(), e.getValue(), e.getValue() < 10))
        .toList();
int lines = report.size();                                  // 3
String first = report.getFirst().sku();                     // "cup"
boolean mugReorder = report.get(1).reorder();               // true

We picked a stream because the job filters, sorts and maps. For a nightly job that only writes each entry to a file, an entrySet() loop with a BufferedWriter is the better fit, because it can throw IOException without wrapping it.

6. HashMap Iteration FAQs

Most searches about looping over a HashMap ask about speed, removal or order.

6.1. Which is faster, entrySet() or keySet()?

The entrySet() view is faster when we also need the values. A keySet() loop must call get() for each key, which hashes the key and searches its bucket again. In our JDK 25 run, it took about twice as long on 100 entries. When we need only the keys, keySet() costs the same as entrySet().

6.2. Is Map.forEach() slower than a for-each loop?

No. On JDK 25, HashMap.forEach() was as fast as the entrySet() loop within the error margin, because HashMap overrides it and walks the bucket array without an iterator. We choose between them for readability.

6.3. How do we remove entries from a HashMap while iterating?

We call removeIf() on a view, or remove() on an explicit Iterator. Calling map.remove() inside a for-each loop throws ConcurrentModificationException on the next step.

Map<String, Integer> stock = new HashMap<>(Map.of("tea", 5, "mug", 0));
boolean removed = stock.entrySet().removeIf(e -> e.getValue() == 0);   // true

6.4. Why does a HashMap print its entries in a different order?

A HashMap orders entries by bucket, not by insertion, and the bucket of a key depends on its hash code and the capacity. We use LinkedHashMap for insertion order, TreeMap for sorted keys, or sort the entries in a stream.

6.5. Can we iterate a HashMap in reverse order?

No, because a HashMap has no defined order to reverse. A LinkedHashMap or TreeMap has one, and since Java 21 both offer reversed() through SequencedMap.

TreeMap<String, Integer> sorted = new TreeMap<>(Map.of("tea", 5, "mug", 2, "pot", 3));
String firstKey = sorted.reversed().firstKey();            // "tea"

7. Conclusion

We iterate a HashMap through its views or forEach(). For key and value, a for-each loop over entrySet() or Map.forEach() is the right default, and both ran equally fast on JDK 25.

The keySet() + get() pattern repeats a hash lookup per key and was the slowest way we measured, so we replace it whenever we see it. To delete during a loop we use removeIf() or iterator.remove(), and when order matters we move to LinkedHashMap, TreeMap or a sorted stream. The Java HashMap guide lists the other everyday HashMap tasks.

8. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. So, I’ve found that entry set is only faster after the first loop through (as the entry objects are cached, I’d assume).

    Using entrySet() in for-each loop for key/value #1: 95
    Using entrySet() in for-each loop for key/value #2: 66
    Using keySet() in for-each loop for key/value: 68
    Using entrySet() in for-each loop for key: 33
    Using keySet() in for-each loop for key: 31
    Using entrySet() in for-each loop for value: 33
    Using values() in for-each loop for value: 33

    I’m assuming your example is “optimizing” a bit since many of your methods have no-op code in the loop itself. Add a System.out.println for each of your values so Java can’t ignore what’s happening in the loop and you’ll probably get different results. Also, if you really want to do timing, don’t use the Calendar. System.nanoTime() is much more accurate. (System.currentTimeInMillis() isn’t guaranteed to be consistently as accurate last I checked, anyway)

  2. Hi Lokesh,
    I am little confuse about working of entrySet method.
    public Set<Map.Entry> entrySet() {
    return entrySet0();
    }

    private Set<Map.Entry> entrySet0() {
    Set<Map.Entry> es = entrySet;
    return es != null ? es : (entrySet = new EntrySet());
    }
    When this entrySet is assigning values into this , i can see its called the constructor which is default one.Initially this entrySet is null but when its assigned values into this entrySet.

  3. Great compairsion!

    Thanks for that! That helped me to solve a problem I had using Primefaces bar charts… thanks!

  4. Hi Lokesh,
    Firstly Brilliant article on HashMap working.
    QUERY:Will there be an issue if I create the Hashmap inside the static block itself?

    • I don’t see any adverse effect on HashMap when you create HashMap inside static block. Only one thing concern me is that HashMap instance will also be static; so all the objects it refers from either keys OR values will not be garbage collected by GC for longer time. Usually static variables in application live longer than their non-static counterparts.

  5. Hi Lokesh,

    As per my Analysis, If you want to retrieve some value from Map based on “key” search then keySet would be faster(little bit) But if you want to parse the whole Map then entrySet would be faster 30 to 40 % based on data size. If required then I can post the code also.

  6. Hi Lokesh,

    I also tried to find the best way to parse a Map and found that results are better with keySet. Here I am posting the code that I used for my analysis. Kindly have a look…and please let me know if you find something valuable to share…Thanks in advance..

    —————————————————————————————————–

    package com.himanshu;

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

    public class Test
    {
    static Integer finalKey = 10000;

    public static void main(String[] args)
    {
    Map map = new HashMap();

    for(int i=1 ; i <= 1000000; i++)
    {
    map.put(i, "a");
    }

    long first = parseThroughEntrySet(map);

    long second = parseThroughKeySet(map);

    System.out.println("First :"+first);
    System.out.println("Second :"+second);

    }

    static long parseThroughEntrySet(Map map)
    {
    long startTime = System.currentTimeMillis();

    String value;

    for (Entry entry : map.entrySet())
    {
    Integer key = entry.getKey();

    if(key.equals(finalKey))
    {
    value = entry.getValue();
    }

    }

    long endTime = System.currentTimeMillis();

    long totalTime = endTime – startTime;

    return totalTime;
    }

    static long parseThroughKeySet(Map map)
    {
    long startTime = System.currentTimeMillis();

    String value;

    for(Integer key : map.keySet())
    {
    if(key.equals(finalKey))
    {
    value = map.get(key);
    System.out.println(“Done in parseThroughKeySet !!!” +value);
    }
    }

    long endTime = System.currentTimeMillis();

    long totalTime = endTime – startTime;

    return totalTime;
    }

    }

    ————————————————————————————————————————————–

  7. Hi ,

    I have tried this and come with different results everytime , however i find that results are better with key set , even i read somewhere that key set is more better than entry set .

    Regards,
    chandra

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.