Skip to content

How to Iterate Over a Map in Java: entrySet and forEach

Loop over a Java Map the right way: entrySet, forEach, iterator removal, sorted output and the exceptions to expect, built on a small currency-rates feed.

how-to-tutorials java-collections java-tutorials
Gopi Gorantala
Reading Progress

On This Page

Start with the printed rates

This is what the finished program prints when it walks a currency-rates map five different ways, edits it while walking, and then breaks it on purpose:

-- entrySet() for-each
EUR -> 0.9215
GBP -> 0.7890
JPY -> 149.82
INR -> 83.27
-- iterator remove (drop rates above 100)
{EUR=0.9215, GBP=0.7890, INR=83.27}
-- setValue on entry
{EUR=0.930715, INR=84.1027, CHF=0.890012}
-- CME trap
CME: java.util.ConcurrentModificationException

If you want to know how to iterate over a map in Java, the short answer is for (Map.Entry<K, V> e : map.entrySet()), or map.forEach((k, v) -> ...) when you don't need to remove anything. The rest of this article is about the cases where that answer isn't enough: removing while looping, changing values, getting sorted output, and the three or four ways a loop over a map throws at you.

The example is a currency-rates feed: currency codes as keys, BigDecimal rates as values. It's one class, one checks file, and no build tool. I did it on plain javac because Maven Central wasn't reachable from my build box this run, so there's no JUnit here; the checks are a small hand-written runner you can paste in and later convert to JUnit.

Budget about 25 minutes. The build took me less than that; most of the time went into working out why one of my "this should throw" examples didn't (it's in the troubleshooting section, and it's a good one).

Versions I used

Tool Version I used Check it with
JDK OpenJDK 21.0.12.1 (Ubuntu build) java -version
Compiler flags --release 21 -Xlint:all javac --release 21 ...
Build tool none

Main needs Java 10 or newer (it uses var), and Checks uses Java 21's SequencedMap. I compiled Main with --release 17 as well and it was clean. You should know what a Map is and how generics read; nothing else.

Need a JDK? On macOS or Linux, sdk install java 21.0.12-tem through SDKMAN works (check sdk list java for the exact identifier on your machine; I didn't run this one).

Create the project

I'd use the command line here, because it's two files. Then IntelliJ IDEA and Eclipse are a minute each.

mkdir -p rates/src/com/example/rates && cd rates

The tree when you're done:

rates/
└── src/com/example/rates/
    ├── Main.java
    └── Checks.java

In IntelliJ IDEA (not executed here, from the vendor docs): File → New → Project, choose Java, pick JDK 21, untick the sample-code box, and name it rates. Create the package com.example.rates under src and paste the files in.

In Eclipse (docs only): File → New → Java Project, select a JRE of 21, then right-click src → New → Package → com.example.rates. Paste the files in; Run As → Java Application.

How to iterate over a Map in Java, one pattern at a time

Every step edits the same Main.java. The complete file is below; I'll then walk through which part answers which question.

Step 1: Write the whole program

src/com/example/rates/Main.java

package com.example.rates;

import java.math.BigDecimal;
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, BigDecimal> rates = new LinkedHashMap<>();
        rates.put("EUR", new BigDecimal("0.9215"));
        rates.put("GBP", new BigDecimal("0.7890"));
        rates.put("JPY", new BigDecimal("149.82"));
        rates.put("INR", new BigDecimal("83.27"));

        System.out.println("-- entrySet() for-each");
        for (Map.Entry<String, BigDecimal> e : rates.entrySet()) {
            System.out.println(e.getKey() + " -> " + e.getValue());
        }
        System.out.println("-- Map.forEach");
        rates.forEach((code, rate) -> System.out.println(code + " -> " + rate));
        System.out.println("-- keySet() then get()");
        for (String code : rates.keySet()) {
            System.out.println(code + " -> " + rates.get(code));
        }
        System.out.println("-- values()");
        BigDecimal sum = BigDecimal.ZERO;
        for (BigDecimal r : rates.values()) sum = sum.add(r);
        System.out.println("sum = " + sum);

        System.out.println("-- iterator remove (drop rates above 100)");
        for (Iterator<Map.Entry<String, BigDecimal>> it = rates.entrySet().iterator(); it.hasNext(); ) {
            if (it.next().getValue().compareTo(new BigDecimal("100")) > 0) it.remove();
        }
        System.out.println(rates);

        System.out.println("-- removeIf on entrySet");
        rates.put("CHF", new BigDecimal("0.8812"));
        rates.entrySet().removeIf(e -> e.getKey().equals("GBP"));
        System.out.println(rates);

        System.out.println("-- setValue on entry");
        for (Map.Entry<String, BigDecimal> e : rates.entrySet()) {
            e.setValue(e.getValue().multiply(new BigDecimal("1.01")));
        }
        System.out.println(rates);

        System.out.println("-- replaceAll");
        rates.replaceAll((k, v) -> v.setScale(2, java.math.RoundingMode.HALF_UP));
        System.out.println(rates);

        System.out.println("-- sorted by value, streams");
        rates.entrySet().stream()
             .sorted(Map.Entry.comparingByValue())
             .forEach(e -> System.out.println(e.getKey() + " " + e.getValue()));

        System.out.println("-- TreeMap sorted keys");
        new TreeMap<>(rates).forEach((k, v) -> System.out.println(k + " " + v));

        System.out.println("-- HashMap order is not insertion order");
        Map<String, Integer> h = new HashMap<>();
        for (String s : List.of("USD", "EUR", "GBP", "JPY", "INR", "CHF", "AUD")) h.put(s, s.length());
        System.out.println(h.keySet());

        System.out.println("-- CME trap");
        try {
            for (String k : rates.keySet()) if (k.equals("EUR")) rates.remove(k);
            System.out.println("no CME: " + rates);
        } catch (ConcurrentModificationException ex) { System.out.println("CME: " + ex); }
        Map<String, Integer> m = new LinkedHashMap<>(Map.of("a", 1));
        m.put("b", 2); m.put("c", 3);
        try {
            for (String k : m.keySet()) if (k.equals("a")) m.remove(k);
        } catch (ConcurrentModificationException ex) { System.out.println("CME: " + ex); }
        try {
            for (String k : m.keySet()) m.put(k + k, 0);
        } catch (ConcurrentModificationException ex) { System.out.println("CME on put: " + ex); }

        System.out.println("-- immutable Map.of entry setValue");
        try {
            for (var e : Map.of("x", 1).entrySet()) e.setValue(2);
        } catch (UnsupportedOperationException ex) { System.out.println("UOE: " + ex); }

        System.out.println("-- null value / entry kept past loop");
        Map<String,Integer> hm = new HashMap<>(); hm.put("k", 1);
        Map.Entry<String,Integer> kept = null;
        for (var e : hm.entrySet()) kept = e;
        hm.put("k", 99);
        System.out.println("entry after put: " + kept);
        Map.Entry<String,Integer> copy = Map.entry("k", 1);
        System.out.println("immutable copy: " + copy + " (use Map.entry or new AbstractMap.SimpleEntry to keep one)");
    }
}

Compile and run:

mkdir -p out
javac --release 21 -Xlint:all -d out src/com/example/rates/*.java
java -cp out com.example.rates.Main

On Windows PowerShell the *.java glob can be refused by javac; list the two files by name.

Checkpoint: the compile prints nothing (that's the point of -Xlint:all). The run starts with -- entrySet() for-each and the four rates in insertion order.

Step 2: Know which loop to reach for

Three loops give you the same output on the first run, so it's fair to ask why there are three.

  • entrySet() hands you a key and its value together. One pass over the table. This is my default.
  • keySet() plus get() looks the key up again on every turn. It works, and it's what most older tutorials teach. On a HashMap each get is a second hash lookup.
  • values() is for when you don't need the key at all (the sum in the program).

Is the difference real? On a two-million-entry HashMap<Integer, Integer>, on my box, three rounds:

keySet+get 78 ms | entrySet 40 ms
keySet+get 68 ms | entrySet 16 ms
keySet+get 44 ms | entrySet 15 ms

That's an unscientific timing (no JMH, JIT still warming up), so read it as "entrySet isn't slower, and usually isn't close". For a map with twenty entries you'll never notice. I still write entrySet() because I don't want to think about it later.

Step 3: Use forEach when you only read

map.forEach((code, rate) -> ...) takes a two-argument lambda. It's the shortest correct loop and the output above shows it matches entrySet() exactly. Pass one parameter by mistake and you get this:

C.java:2: error: incompatible types: incompatible parameter types in lambda expression

Don't modify the map inside the lambda either. My test, which puts a new key from inside forEach:

Exception in thread "main" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1432)

Step 4: Remove while looping — with the iterator, or removeIf

Calling map.remove(k) inside a for-each loop is the classic mistake. Two safe versions are in the program:

for (Iterator<Map.Entry<String, BigDecimal>> it = rates.entrySet().iterator(); it.hasNext(); ) {
    if (it.next().getValue().compareTo(new BigDecimal("100")) > 0) it.remove();
}

and the shorter rates.entrySet().removeIf(e -> e.getKey().equals("GBP")). Both write through to the map. Prefer removeIf unless you need the iterator for something else.

Checkpoint: after the iterator version, JPY is gone: {EUR=0.9215, GBP=0.7890, INR=83.27}. After removeIf, so is GBP and the new CHF entry is last: {EUR=0.9215, INR=83.27, CHF=0.8812}.

Step 5: Change values in place

You can call setValue on an entry that comes out of a HashMap, LinkedHashMap or TreeMap's entrySet(), and the map sees it. Or skip the loop with replaceAll:

-- setValue on entry
{EUR=0.930715, INR=84.1027, CHF=0.890012}
-- replaceAll
{EUR=0.93, INR=84.10, CHF=0.89}

Changing a value during iteration is fine. Adding or removing a key is the problem, and that's next.

Step 6: Get sorted output

A HashMap doesn't promise any order. Mine printed [CHF, AUD, JPY, EUR, GBP, USD, INR] for keys I inserted as USD, EUR, GBP, JPY, INR, CHF, AUD. Don't rely on yours matching.

  • Insertion order: use a LinkedHashMap (see how to use LinkedHashMap).
  • Sorted by key: wrap in new TreeMap<>(map). TreeMap rejects null keys with a NullPointerException; HashMap takes one.
  • Sorted by value: stream the entrySet() through Map.Entry.comparingByValue(). To keep that order in a map, collect into a LinkedHashMap, otherwise you've re-hashed it (collecting stream results, and Comparable and Comparator for custom ordering).

Check it works end to end

src/com/example/rates/Checks.java

package com.example.rates;

import java.util.*;
import java.util.stream.*;

public class Checks {
    static int passed, failed;
    static void check(String name, boolean ok) {
        if (ok) passed++; else { failed++; System.out.println("FAIL: " + name); }
    }
    static <T extends Throwable> void throwsA(String name, Class<T> type, Runnable r) {
        try { r.run(); check(name, false); }
        catch (Throwable t) { check(name, type.isInstance(t)); }
    }
    public static void main(String[] args) {
        Map<String,Integer> m = new LinkedHashMap<>();
        m.put("EUR", 1); m.put("GBP", 2); m.put("JPY", 3);

        check("entrySet order", m.entrySet().stream().map(Map.Entry::getKey).collect(Collectors.toList()).equals(List.of("EUR","GBP","JPY")));
        List<String> seen = new ArrayList<>();
        m.forEach((k, v) -> seen.add(k + v));
        check("forEach pairs", seen.equals(List.of("EUR1","GBP2","JPY3")));

        for (Map.Entry<String,Integer> e : m.entrySet()) e.setValue(e.getValue() * 10);
        check("setValue writes through", m.get("GBP") == 20);

        Iterator<Map.Entry<String,Integer>> it = m.entrySet().iterator();
        while (it.hasNext()) if (it.next().getKey().equals("GBP")) it.remove();
        check("iterator remove", !m.containsKey("GBP") && m.size() == 2);

        m.put("GBP", 2);
        m.entrySet().removeIf(e -> e.getValue() > 15);
        check("removeIf", m.keySet().equals(Set.of("EUR","GBP")));

        throwsA("remove in for-each", ConcurrentModificationException.class, () -> {
            Map<String,Integer> t = new HashMap<>(Map.of("a",1,"b",2,"c",3));
            for (String k : t.keySet()) t.remove(k);
        });
        throwsA("put new key in for-each", ConcurrentModificationException.class, () -> {
            Map<String,Integer> t = new LinkedHashMap<>();
            t.put("a", 1); t.put("b", 2);
            for (String k : t.keySet()) t.put(k + "x", 0);
        });
        Map<String,Integer> one = new LinkedHashMap<>(Map.of("a",1));
        for (String k : one.keySet()) one.put(k + "x", 0);
        check("one-entry map: put new key does NOT throw", one.size() == 2);
        Map<String,Integer> same = new HashMap<>(Map.of("a",1));
        for (String k : same.keySet()) same.put(k, 5);
        check("put existing key is fine", same.get("a") == 5);

        throwsA("Map.of entry immutable", UnsupportedOperationException.class, () -> {
            for (var e : Map.of("x",1).entrySet()) e.setValue(2);
        });
        throwsA("Map.entry immutable", UnsupportedOperationException.class, () -> Map.entry("k",1).setValue(2));

        Map<String,Integer> tm = new TreeMap<>(Comparator.reverseOrder());
        tm.putAll(Map.of("a",1,"b",2,"c",3));
        check("TreeMap reverse", new ArrayList<>(tm.keySet()).equals(List.of("c","b","a")));

        List<String> byVal = Map.of("x",3,"y",1,"z",2).entrySet().stream()
            .sorted(Map.Entry.comparingByValue()).map(Map.Entry::getKey).toList();
        check("sorted by value", byVal.equals(List.of("y","z","x")));

        Map<String,Integer> sorted = Map.of("x",3,"y",1,"z",2).entrySet().stream()
            .sorted(Map.Entry.comparingByValue())
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a,b)->a, LinkedHashMap::new));
        check("collect into LinkedHashMap keeps sort", new ArrayList<>(sorted.keySet()).equals(List.of("y","z","x")));

        throwsA("null key in TreeMap", NullPointerException.class, () -> new TreeMap<String,Integer>().put(null, 1));
        Map<String,Integer> hm = new HashMap<>(); hm.put(null, 1);
        check("HashMap allows null key", hm.get(null) == 1);

        SequencedMap<String,Integer> sm = new LinkedHashMap<>();
        sm.put("a",1); sm.put("b",2);
        check("reversed view", sm.reversed().firstEntry().getKey().equals("b"));

        Map<String,Integer> big = new HashMap<>();
        for (int i = 0; i < 100; i++) big.put("k" + i, i);
        long a = 0, b = 0;
        for (String k : big.keySet()) a += big.get(k);
        for (var e : big.entrySet()) b += e.getValue();
        check("both loops same sum", a == b && a == 4950);

        System.out.println("passed=" + passed + " failed=" + failed);
        if (failed > 0) System.exit(1);
    }
}
java -cp out com.example.rates.Checks
passed=18 failed=0

Each check is one thing I claimed above. If you move to JUnit 5 later, each check(...) becomes an @Test with AssertJ's assertThat; I haven't pinned versions for that here because I couldn't resolve them this run.

What the iterator is doing

entrySet(), keySet() and values() aren't copies. They're views onto the map. That's why it.remove() and setValue change the map you started with, and why Map.of("x", 1).entrySet() fails when you try to set a value:

UOE: java.lang.UnsupportedOperationException: not supported

The for-each loop is an Iterator underneath. The map counts structural changes (adds and removes of keys). The iterator remembers the count when it started, and every next() compares. If they differ, you get ConcurrentModificationException. put on an existing key isn't structural, and the checks confirm that; put of a new key is. For the concurrent case, ConcurrentHashMap's iterators don't throw this, but they're weakly consistent (how to use ConcurrentHashMap).

One more consequence: the entry you get in the loop is tied to the map. I held on to the last one from a HashMap, put a new value for the key after the loop, and printed it:

entry after put: k=99

If you need a snapshot, copy it: Map.entry(k, v) (immutable) or new AbstractMap.SimpleEntry<>(k, v). And since equals/hashCode decide how keys are found, get them right on custom key types.

Errors I hit, and the fixes

java.util.ConcurrentModificationException when removing in a for-each. The map changed structure under the iterator. Use it.remove() or removeIf, or loop over a copy: new ArrayList<>(map.keySet()).

It didn't throw on a one-entry map. My first check ("put a new key inside the loop throws") failed on a map with one entry. After the single next() the iterator already had no next element, so the loop ended before the modification count was ever checked. Output of my first run: FAIL: put new key in for-each. Two entries and it throws. The check now has both cases. That's the dangerous kind of bug: green in a quick test, red in production with more data.

for-each not applicable to expression type. You wrote for (var e : map). A Map isn't Iterable; loop over map.entrySet().

wrong number of type arguments; required 2. You wrote Map.Entry<String>. It takes both, Map.Entry<String, Integer>.

NullPointerException from a TreeMap. Null keys aren't allowed there. A HashMap accepts one.

UnsupportedOperationException from setValue. The map came from Map.of, Map.entry or Collections.unmodifiableMap. Copy into a HashMap first. See immutable classes for why those maps are built that way.

Before this goes near production

  • Prefer entrySet() over keySet() + get() as a habit.
  • If the map is shared between threads, a plain HashMap loop can fail in worse ways than an exception. Use ConcurrentHashMap, or don't share it.
  • Java 21 adds SequencedMap on LinkedHashMap and TreeMap: reversed() gives a reversed view (checked, reversed().firstEntry() is the last key). On Java 17 or older that call won't compile. Main itself compiles with --release 17.
  • Next to read: PriorityQueue and TreeMap and unit tests with JUnit 5; both are on the way.

The short version

  • Read pairs with entrySet() or forEach. Read values only with values().
  • Remove with removeIf or the iterator's remove(). Never map.remove inside a for-each.
  • Changing values is fine; adding or removing keys isn't.
  • Sort by streaming the entries, and collect into a LinkedHashMap if the order has to survive.
  • Test with more than one entry.

Questions people ask about looping over a map

What is the best way to iterate over a Map in Java? for (Map.Entry<K, V> e : map.entrySet()) for general use, map.forEach((k, v) -> ...) when you only read. Both are one pass over the entries.

Can I remove entries from a Map while iterating? Yes, through the iterator's remove() or entrySet().removeIf(...). Calling map.remove directly inside a for-each loop throws ConcurrentModificationException in most cases.

How do I iterate a Map in sorted order? Copy it into a TreeMap for key order, or stream entrySet() with Map.Entry.comparingByValue() for value order. Collect into a LinkedHashMap to keep the result.

Why is my HashMap order different from insertion order? HashMap makes no ordering promise. Use LinkedHashMap for insertion order.

References

how-to-tutorialsjava-collectionsjava-tutorials

Staff engineer architecting Kafka and Flink platforms for banking, where a dropped event is a reconciliation break, not a retry. Through javahandbook.com, I share production-tested guidance on building reliable event-driven systems.

Comments