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()plusget()looks the key up again on every turn. It works, and it's what most older tutorials teach. On aHashMapeachgetis 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).TreeMaprejects null keys with aNullPointerException;HashMaptakes one. - Sorted by value: stream the
entrySet()throughMap.Entry.comparingByValue(). To keep that order in a map, collect into aLinkedHashMap, 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()overkeySet()+get()as a habit. - If the map is shared between threads, a plain
HashMaploop can fail in worse ways than an exception. UseConcurrentHashMap, or don't share it. - Java 21 adds
SequencedMaponLinkedHashMapandTreeMap:reversed()gives a reversed view (checked,reversed().firstEntry()is the last key). On Java 17 or older that call won't compile.Mainitself 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()orforEach. Read values only withvalues(). - Remove with
removeIfor the iterator'sremove(). Nevermap.removeinside a for-each. - Changing values is fine; adding or removing keys isn't.
- Sort by streaming the entries, and collect into a
LinkedHashMapif 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
- Java SE 21 API:
java.util.Map,Map.Entry,HashMap,TreeMap,LinkedHashMap,SequencedMap,ConcurrentModificationException. https://docs.oracle.com/en/java/javase/21/docs/api/ - JEP 431: Sequenced Collections. https://openjdk.org/jeps/431