For-Each Loop in Java: A Practical, Modern Guide (Java 17+)

The first time I see a bug caused by \"one too many\" in a loop, I’m reminded why the for-each loop is one of Java’s best readability features. Most iteration in production code isn’t about clever indexing—it’s about walking a set of values, validating them, aggregating them, and moving on. When you write loops that focus on the element instead of the counter, you remove an entire category of mistakes: off-by-one boundaries, wrong start index, wrong increment, and confusing index math.\n\nIf you already write Java daily, you probably use for-each a lot. The payoff comes when you understand exactly what it can and cannot do—especially around mutation, removal, ordering, and performance characteristics across arrays vs collections. I’ll show you how the loop works mechanically, where it shines, where it backfires, and the patterns I recommend in modern Java codebases (think Java 17+ style) so your iteration stays predictable, safe, and easy to review.\n\n## The Mental Model: Elements, Not Indexes\nA for-each loop (often called the \“enhanced for loop\”) is Java’s way of saying: \“Give me each element in this thing, in iteration order.\” You don’t control the index; you don’t ask for it; you don’t update it. You write code against the current element, and Java handles traversal.\n\nThe canonical shape looks like this:\n\n for (ElementType element : arrayOrIterable) {\n // work with element\n }\n\nHere’s a runnable example iterating over an int[]:\n\n class ForEachArrayDemo {\n public static void main(String[] args) {\n int[] scores = { 1, 2, 3, 4, 5 };\n\n for (int score : scores) {\n System.out.print(score + " ");\n }\n }\n }\n\nWhat I like about this form is that it makes intent obvious: you’re processing values. No counter variable is \“in the air,\” so a reader doesn’t wonder whether the index matters.\n\nA simple analogy I use with juniors: an index-based loop is like walking down a row of mailboxes while counting doors; for-each is like reading each letter as it comes out of the bag. If the door number isn’t part of the task, don’t pay the mental cost of tracking it.\n\n### What can you iterate with for-each?\nFor-each works with:\n\n- Arrays (primitive and reference arrays)\n- Any type that implements Iterable (which includes almost all collection types you use daily)\n\nThat “Iterable” detail matters, because it explains most of the behavior differences between iterating an array and iterating a collection.\n\n## Syntax, Desugaring, and What the JVM Actually Runs\nThe for-each loop is syntax sugar. That doesn’t make it “less real”; it just means Java rewrites it into something more explicit.\n\n### Arrays: rewritten as an index loop\nWhen you do:\n\n for (int value : scores) {\n // …\n }\n\nJava compiles that roughly like this (conceptually):\n\n for (int i = 0; i < scores.length; i++) {\n int value = scores[i];\n // …\n }\n\nImportant consequences:\n\n- The loop variable (value) is assigned from the array element each iteration.\n- That variable is not an alias to the array slot; it’s a local variable.\n\n### Collections: rewritten as an Iterator loop\nWhen you do:\n\n for (String name : names) {\n // …\n }\n\nJava compiles that roughly like:\n\n for (java.util.Iterator it = names.iterator(); it.hasNext(); ) {\n String name = it.next();\n // …\n }\n\nThis explains two behaviors you see in real code:\n\n- The iteration order is the collection’s iterator order (e.g., ArrayList is insertion order, HashSet is not guaranteed, LinkedHashSet is stable).\n- Many collections are “fail-fast”: if you structurally modify them while iterating (outside of iterator removal), you’ll usually get ConcurrentModificationException.\n\nIf you remember nothing else: for-each over collections is fundamentally iterator-driven.\n\n## Arrays: Reading, Mutating, and the “Why Didn’t It Change?” Trap\nArrays are the easiest place to start because the order is obvious and access is constant time.\n\n### A practical example: find the maximum value\nThis is a great fit for for-each because the index is irrelevant:\n\n class MaxInArray {\n public static void main(String[] args) {\n int[] marks = { 125, 132, 95, 116, 110 };\n System.out.println(findMax(marks));\n }\n\n static int findMax(int[] values) {\n int max = values[0];\n for (int v : values) {\n if (v > max) {\n max = v;\n }\n }\n return max;\n }\n }\n\n### The common trap: “I reassigned the loop variable, but the array didn’t change”\nIf you do this:\n\n int[] marks = { 10, 20, 30 };\n for (int m : marks) {\n m = m 2; // changes only the local variable m\n }\n\nThe array stays the same, because m is a copy of the element value.\n\n#### How to correctly modify a primitive array\nIf you need to write back into a primitive array, you need an index:\n\n class DoubleArrayValues {\n public static void main(String[] args) {\n int[] marks = { 10, 20, 30 };\n\n for (int i = 0; i < marks.length; i++) {\n marks[i] = marks[i] 2;\n }\n\n for (int v : marks) {\n System.out.print(v + " ");\n }\n }\n }\n\nThis is one of the clearest “use for-each vs don’t” dividing lines: if you must update array slots, prefer the classic indexed loop.\n\n### Reference arrays behave differently (and that’s another trap)\nWith an array of objects, you can mutate the object but you still can’t rebind the slot through the loop variable.\n\n- Allowed (mutate object state): person.setActive(true)\n- Not effective (reassign reference): person = new Person(...) won’t change the array\n\nIf you need to replace elements, use an index loop.\n\n### Multi-dimensional arrays: for-each is great for traversal, not for rewiring\nAs soon as you have int[][] or String[][], the for-each loop reads naturally. I reach for it when I’m scanning a matrix or computing totals.\n\nExample: sum all cells and track a per-row sum:\n\n class MatrixSum {\n static int sumAll(int[][] grid) {\n int total = 0;\n for (int[] row : grid) {\n for (int cell : row) {\n total += cell;\n }\n }\n return total;\n }\n\n static int[] perRowSums(int[][] grid) {\n int[] sums = new int[grid.length];\n for (int r = 0; r < grid.length; r++) {\n int rowSum = 0;\n for (int cell : grid[r]) {\n rowSum += cell;\n }\n sums[r] = rowSum;\n }\n return sums;\n }\n }\n\nNotice how the first method uses pure for-each (values only), while the second needs an index because we’re writing back into a separate array by row position. That’s a recurring theme: when positions matter, indices come back.\n\n## Collections: Iterable, Iterator, and Fail-Fast Behavior\nCollections are where for-each really improves everyday readability—until you accidentally fight the iterator.\n\n### Example: iterating a list to compute a maximum\nThis is the same pattern as the array example, and it reads well:\n\n import java.util.ArrayList;\n import java.util.List;\n\n class MaxInList {\n public static void main(String[] args) {\n List readings = new ArrayList();\n readings.add(3);\n readings.add(5);\n readings.add(7);\n readings.add(9);\n\n int max = Integer.MIN_VALUE;\n for (int r : readings) {\n if (r > max) {\n max = r;\n }\n }\n\n System.out.println("Readings: " + readings);\n System.out.println("Max: " + max);\n }\n }\n\n### The removal problem (and the right way)\nIf you try to remove from many collections inside a for-each, you’ll often get ConcurrentModificationException:\n\n // Don’t do this\n for (String id : ids) {\n if (id.startsWith("tmp-")) {\n ids.remove(id);\n }\n }\n\nThe fix is to either:\n\n1) Use an explicit iterator and iterator.remove():\n\n import java.util.Iterator;\n import java.util.List;\n\n class RemoveWithIterator {\n static void removeTempIds(List ids) {\n for (Iterator it = ids.iterator(); it.hasNext(); ) {\n String id = it.next();\n if (id.startsWith("tmp-")) {\n it.remove(); // safe structural removal\n }\n }\n }\n }\n\n2) Or use removeIf when your intent is filtering:\n\n ids.removeIf(id -> id.startsWith("tmp-"));\n\nIn code reviews, I prefer removeIf when it communicates intent cleanly. I reach for an iterator when removal is interleaved with more complex stateful logic.\n\n### Iterating maps: prefer entries\nWhen you’re walking a Map, iterate entrySet() unless you have a strong reason not to:\n\n- for (var entry : map.entrySet()) gives you key and value together.\n- Iterating keySet() and doing map.get(key) can add extra lookups.\n\nExample:\n\n import java.util.Map;\n\n class MapIteration {\n public static void main(String[] args) {\n Map stock = Map.of(\n "SSD", 12,\n "Monitor", 3,\n "Keyboard", 25\n );\n\n for (Map.Entry e : stock.entrySet()) {\n System.out.println(e.getKey() + ": " + e.getValue());\n }\n }\n }\n\n### Iterating maps when you need to update values\nA subtle gotcha: iterating entries is also the cleanest way to update values for many Map implementations, but only if the update is allowed and you do it correctly.\n\n- If the map is mutable (like HashMap), updating an existing value is typically fine.\n- Structural changes (adding/removing keys) while iterating are still a problem unless you use iterator removal patterns.\n\nExample: cap values in-place without changing keys:\n\n import java.util.HashMap;\n import java.util.Map;\n\n class CapInventory {\n static void capAt(Map stock, int cap) {\n for (var e : stock.entrySet()) {\n int v = e.getValue();\n if (v > cap) {\n e.setValue(cap);\n }\n }\n }\n\n public static void main(String[] args) {\n Map stock = new HashMap();\n stock.put("SSD", 12);\n stock.put("Monitor", 3);\n stock.put("Keyboard", 25);\n\n capAt(stock, 10);\n System.out.println(stock);\n }\n }\n\nIf you try to do this through keySet() + put, you can accidentally mix reads and structural updates in a way that becomes harder to reason about. Entries keep it tight and explicit.\n\n## When for-each Is the Best Choice (and When It’s Not)\nI use for-each by default when I’m doing a straightforward traversal. But I’m strict about switching away from it when the task needs index semantics.\n\n### My default recommendation\nUse for-each when:\n\n- You only need values (not positions)\n- You’re reading or aggregating\n- You want clarity more than control\n\nAvoid for-each when:\n\n- You need the index (position-based rules, reporting, pairing with another structure)\n- You need reverse iteration\n- You must replace elements in an array\n- You must remove/insert in many collections (unless you switch to iterator or removeIf)\n\n### A quick comparison table\n

Task

Best default

Why

\n

\n

Print/log each element

for-each

Lowest noise

\n

Compute sum/max/min

for-each

Index irrelevant

\n

Modify int[] in place

index-based for

Need write-back

\n

Remove items from List

removeIf or iterator

Safe structural change

\n

Need element position

index-based for or indexed stream

Position is part of logic

\n

Reverse order over array/list

index-based for or ListIterator

Direction control

\n\n### Getting an index without abandoning readability\nIf you really want “for-each feel” but need positions, you have a few options:\n\n1) Plain indexed loop (still the clearest in many cases):\n\n for (int i = 0; i System.out.println(i2 + ": " + names.get(i2)));\n\nI don’t recommend the counter trick when the index drives correctness (because it’s still easy to desync). If the index matters, I prefer an indexed loop.\n\n## Common Mistakes I See in Code Reviews\nThese patterns show up constantly, even among experienced developers.\n\n### 1) Shadowing or reusing a loop variable name\nThis is subtle and hurts readability:\n\n- Outer user variable exists\n- Inner loop also uses user\n\nKeep loop variable names specific: order, orderId, lineItem, priceCents. Make the loop read like a sentence.\n\n### 2) Treating the loop variable as “live” back into the data structure\nThis is the mutation trap discussed earlier. If you’re iterating an array of primitives, reassignment never updates the array. If you’re iterating references, reassignment never replaces the slot.\n\nA quick rule I follow: if I need to replace something “in the container,” I don’t use for-each.\n\n### 3) Removing from a collection during for-each\nIf you need to delete while iterating:\n\n- Prefer removeIf for filtering\n- Prefer an explicit iterator for stateful removal\n\n### 4) Assuming ordering where none is guaranteed\nIf you write:\n\n for (String tag : new java.util.HashSet(tags)) { … }\n\nand your tests assume stable order, you’ll get flaky behavior. If order matters, choose an ordered collection (ArrayList, LinkedHashSet) or sort before iterating.\n\n### 5) Doing expensive work inside the loop without realizing it\nThe loop form isn’t the issue; hidden complexity is. Common cases:\n\n- Calling a database/service per element (N calls)\n- Calling list.indexOf(element) inside the loop (O(n^2))\n\nWhen I see this, I refactor toward precomputation or indexing structures (like a Map) so each iteration stays cheap.\n\n## Performance Notes and Memory Behavior in Real Systems\nIn most application code, for-each vs index-based loops won’t be your bottleneck. Still, there are a few practical performance rules I actually use.\n\n### Arrays: for-each is usually as fast as you need\nFor arrays, for-each compiles down to an indexed loop, so performance is typically on par with what you’d write by hand.\n\n### Lists: for-each is great on ArrayList, risky on LinkedList for random access\n- For-each over ArrayList uses an iterator; it’s fast and predictable.\n- The real danger is not for-each; it’s writing an index loop over a LinkedList:\n\n // Often slow: each get(i) can walk the list\n for (int i = 0; i < linkedList.size(); i++) {\n linkedList.get(i);\n }\n\nFor LinkedList, for-each (iterator-driven) is commonly the better choice.\n\n### Autoboxing can dominate in hot loops\nIf you iterate List in a tight loop, you’re dealing with boxed integers. Sometimes that’s fine. If it’s a high-volume, low-latency path, you may want primitives (int[]) or specialized structures.\n\nI’m not telling you to rewrite everything into primitive arrays. I’m saying: if a profiler points at boxing overhead, remember that for-each over boxed types makes that overhead easy to miss.\n\n### Concurrency and visibility\nFor-each doesn’t change thread-safety rules. If the underlying collection can change concurrently, you need the right data structure (like CopyOnWriteArrayList for read-heavy cases) or synchronization. Otherwise, you may see exceptions or inconsistent iteration.\n\n## Modern Patterns: Streams, Pattern Matching, and Hybrid Loops\nIn modern Java, you have three mainstream iteration styles: index loops, for-each loops, and streams. I don’t treat streams as “better”; I treat them as a different tool.\n\n### My practical decision rule\n- If you’re doing a simple pass with a bit of logic: for-each.\n- If you’re transforming/filtering/collecting in a pipeline: streams.\n- If you need index math or in-place updates: index loop.\n\n### A real-world example: validation plus reporting\nIf you need to validate items and collect errors, I often prefer for-each because it’s easy to step through and easy to add instrumentation:\n\n import java.util.ArrayList;\n import java.util.List;\n\n class Validator {\n static List validateSkus(List skus) {\n List errors = new ArrayList();\n\n for (String sku : skus) {\n if (sku == null

sku.isBlank()) {\n errors.add("SKU is blank");\n continue;\n }\n if (!sku.matches("[A-Z]{3}-\\\\d{4}")) {\n errors.add("Invalid SKU format: " + sku);\n }\n }\n\n return errors;\n }\n }\n\nYou can write that with streams, but the moment you need multiple error types, counters, and logs, the loop is often clearer.\n\n### Streams when you’re transforming data, not controlling flow\nWhere streams shine is the \“shape\” of the operation: filter, map, group, collect. If my goal is \“produce a new thing from this thing\” (and I don’t need tricky control flow like labeled breaks), streams can be a clean, declarative fit.\n\nExample: normalize input, drop blanks, and deduplicate while preserving first-seen order:\n\n import java.util.LinkedHashSet;\n import java.util.List;\n import java.util.Set;\n import java.util.stream.Collectors;\n\n class Normalizer {\n static Set normalizeTags(List tags) {\n return tags.stream()\n .filter(s -> s != null && !s.isBlank())\n .map(String::trim)\n .map(String::toLowerCase)\n .collect(Collectors.toCollection(LinkedHashSet::new));\n }\n }\n\nI still keep for-each as my default for day-to-day procedural work, but I like streams for these \“pipeline\” problems because they turn a multi-step loop into a readable sequence of transformations.\n\n### Hybrid approach: streams to prepare, for-each to execute\nIn real systems, I often do a little functional prep and then a straightforward loop for the side effects (API calls, logging, writes). It avoids the \“streams used as fancy loops\” smell while still capturing the transformation intent.\n\nExample: build a list of work items, then process them with explicit error handling:\n\n import java.util.List;\n import java.util.stream.Collectors;\n\n class WorkRunner {\n static void runAll(List rawIds) {\n List ids = rawIds.stream()\n .filter(s -> s != null && !s.isBlank())\n .map(String::trim)\n .distinct()\n .collect(Collectors.toList());\n\n for (String id : ids) {\n try {\n runOne(id);\n } catch (RuntimeException ex) {\n System.err.println("Failed id=" + id + ": " + ex.getMessage());\n }\n }\n }\n\n static void runOne(String id) {\n // pretend this calls something external\n }\n }\n\nThat structure gives me the best of both: concise shaping plus clear, debuggable execution.\n\n## Control Flow Superpowers: break, continue, and Labeled Loops\nOne reason I’m still bullish on for-each is that it plays extremely well with classic control flow. When the logic is inherently procedural, the loop reads like a story.\n\n### break and continue\nThese are simple, but they matter. They’re often what keeps a loop readable because they avoid deeply nested if blocks.\n\nExample: stop at the first invalid value and return early:\n\n class FirstInvalid {\n static String firstInvalidSku(Iterable skus) {\n for (String sku : skus) {\n if (sku == null sku.isBlank()) {\n return "blank";\n }\n if (!sku.matches("[A-Z]{3}-\\\\d{4}")) {\n return sku;\n }\n }\n return null;\n }\n }\n\nExample: skip work for elements you don’t care about:\n\n for (String line : lines) {\n if (line.isBlank() line.startsWith("#")) {\n continue;\n }\n // process meaningful line\n }\n\n### Labeled break: escape nested loops cleanly\nLabeled breaks look old-school, but when you’re scanning a nested structure and want to exit both loops the moment you find something, they’re the most direct tool in the language.\n\nExample: find the first match in a 2D grid:\n\n class FindInGrid {\n static int[] findFirst(int[][] grid, int target) {\n int foundRow = -1;\n int foundCol = -1;\n\n search: for (int r = 0; r < grid.length; r++) {\n int[] row = grid[r];\n for (int c = 0; c < row.length; c++) {\n if (row[c] == target) {\n foundRow = r;\n foundCol = c;\n break search;\n }\n }\n }\n\n return (foundRow == -1) ? null : new int[] { foundRow, foundCol };\n }\n }\n\nCould you do this other ways? Sure. But labeled break keeps it honest: this is nested search with early exit.\n\n## Ordering: Iteration Order Is a Contract (Even If You Didn’t Mean It To Be)\nIteration order is one of those things that developers accidentally rely on. Then someone swaps a LinkedHashMap for a HashMap, or the JVM changes an internal detail, and suddenly your tests start failing or your logs become weirdly unstable.\n\n### What I assume about common collections\n- ArrayList: stable iteration order (insertion order, by index)\n- LinkedList: stable iteration order (insertion order)\n- HashSet: no guaranteed order\n- LinkedHashSet: stable insertion order\n- TreeSet: sorted order (natural or comparator)\n- HashMap: no guaranteed order\n- LinkedHashMap: stable insertion order (or access order if configured)\n- TreeMap: sorted by key\n\nIf order affects behavior, I try to encode that into the type choice. If I see HashSet or HashMap and the output is presented to users (or used for deterministic snapshots/tests), I get suspicious.\n\n### Sorting before for-each\nSometimes the right move is to keep the underlying structure as-is (because it’s used for performance elsewhere) but sort a view of it before iterating.\n\nExample: stable printing of tags:\n\n import java.util.ArrayList;\n import java.util.Collections;\n import java.util.Set;\n\n class StableOutput {\n static void printSorted(Set tags) {\n var list = new ArrayList(tags);\n Collections.sort(list);\n\n for (String tag : list) {\n System.out.println(tag);\n }\n }\n }\n\nI like this because it makes the ordering decision explicit right where the iteration happens.\n\n## Mutation: What You Can Change Safely During for-each\nI separate mutation into two categories because they behave differently: mutating elements vs mutating the container.\n\n### Mutating element state is usually fine\nIf you’re iterating over objects, it’s normal to update each object’s fields, call methods, or accumulate state. That doesn’t structurally modify the collection.\n\nExample: mark inactive users (object mutation):\n\n import java.util.List;\n\n class User {\n boolean active;\n void deactivate() { this.active = false; }\n }\n\n class MutateElements {\n static void deactivateAll(List users) {\n for (User u : users) {\n u.deactivate();\n }\n }\n }\n\n### Mutating the container is where fail-fast bites\nStructural changes (add/remove) during a for-each are the main trap because the underlying iterator expects the collection’s structure not to change unexpectedly.\n\nMy rule of thumb:\n- If you’re filtering: removeIf\n- If you’re removing conditionally with complex logic: explicit iterator + it.remove()\n- If you’re adding while iterating: consider collecting additions separately, or rethink the algorithm\n\nExample: split into two lists without mutating during iteration:\n\n import java.util.ArrayList;\n import java.util.List;\n\n class Partition {\n static class Result {\n final List good;\n final List bad;\n\n Result(List good, List bad) {\n this.good = good;\n this.bad = bad;\n }\n }\n\n static Result partitionSkus(List skus) {\n List good = new ArrayList();\n List bad = new ArrayList();\n\n for (String sku : skus) {\n if (sku != null && sku.matches("[A-Z]{3}-\\\\d{4}")) {\n good.add(sku);\n } else {\n bad.add(sku);\n }\n }\n\n return new Result(good, bad);\n }\n }\n\nNo removals, no surprises, and the intent is crystal clear.\n\n## Nulls and Defensive Iteration (a.k.a. “Don’t Let a Loop Be Your NPE Hotspot”)\nFor-each is clean, but it won’t protect you from bad inputs. In production code, the real question is: what’s your contract?\n\n### Decide whether null collections are allowed\nI prefer to enforce \“never null\” at boundaries and keep internal code simple. But if you’re writing utility code, you may need to be defensive.\n\nOption A: enforce non-null at the boundary:\n\n import java.util.List;\n import java.util.Objects;\n\n class NonNullBoundary {\n static int sum(List nums) {\n Objects.requireNonNull(nums, "nums");\n int total = 0;\n for (int n : nums) {\n total += n;\n }\n return total;\n }\n }\n\nOption B: treat null as empty (only if it matches your API expectations):\n\n import java.util.List;\n\n class NullAsEmpty {\n static int sumOrZero(List nums) {\n if (nums == null) {\n return 0;\n }\n int total = 0;\n for (int n : nums) {\n total += n;\n }\n return total;\n }\n }\n\n### Null elements inside a collection\nThis is common with messy inputs. If null elements are possible, make it explicit in the loop. I prefer an early continue because it keeps the main path readable.\n\n for (String s : items) {\n if (s == null) {\n continue;\n }\n // normal logic\n }\n\n## Modern Java Convenience: var in for-each\nIf you’re on Java 10+ (and you are if you’re on Java 17+), you can use var in the enhanced for loop. I use it when the type is obvious from the right-hand side and the variable name carries meaning.\n\nExample: iterating map entries without repeating generic types:\n\n import java.util.Map;\n\n class VarForEach {\n static void print(Map stock) {\n for (var e : stock.entrySet()) {\n System.out.println(e.getKey() + " => " + e.getValue());\n }\n }\n }\n\nWhen I avoid var:\n- When the type is not obvious (hurts readability)\n- When I specifically want the declared type to document intent (e.g., long totalCents)\n\n## Custom Types: How to Make Your Own Class Work with for-each\nA for-each loop works on Iterable. That means you can make your own domain types iterable, which can be a big readability win when you have natural collections embedded in objects.\n\nExample: a tiny Range type that can be used directly in a for-each loop:\n\n import java.util.Iterator;\n import java.util.NoSuchElementException;\n\n class Range implements Iterable {\n private final int startInclusive;\n private final int endExclusive;\n\n Range(int startInclusive, int endExclusive) {\n this.startInclusive = startInclusive;\n this.endExclusive = endExclusive;\n }\n\n @Override\n public Iterator iterator() {\n return new Iterator() {\n private int cur = startInclusive;\n\n @Override\n public boolean hasNext() {\n return cur < endExclusive;\n }\n\n @Override\n public Integer next() {\n if (!hasNext()) {\n throw new NoSuchElementException();\n }\n return cur++;\n }\n };\n }\n\n public static void main(String[] args) {\n for (int i : new Range(3, 7)) {\n System.out.print(i + " ");\n }\n }\n }\n\nThis isn’t something I do everywhere, but when a type has a natural iteration meaning (range, tree traversal, page of results), Iterable gives you for-each readability across the codebase.\n\n## Concurrency: Snapshot Iteration vs Live Iteration\nWhen concurrency enters the picture, the meaning of “for each element” changes. The loop syntax stays the same; the underlying iterator semantics are what matter.\n\n### Fail-fast collections (most common)\nMany standard collections are fail-fast: they’ll throw ConcurrentModificationException if they detect structural changes during iteration (usually changes from the same thread, but cross-thread races can surface too).\n\nThe fix is not \“don’t use for-each.\” The fix is to pick the correct concurrency strategy.\n\n### CopyOnWriteArrayList: stable snapshot, expensive writes\nIn read-heavy scenarios, CopyOnWriteArrayList is a practical tool: iteration sees a snapshot and doesn’t throw CME for concurrent modifications. The trade-off is that writes are expensive (copying).\n\nExample: safe iteration while another thread may add/remove:\n\n import java.util.concurrent.CopyOnWriteArrayList;\n\n class SnapshotIteration {\n static void printAll(CopyOnWriteArrayList list) {\n for (String s : list) {\n System.out.println(s);\n }\n }\n }\n\n### ConcurrentHashMap: weakly consistent iterators\nWith concurrent maps, iterators are typically weakly consistent: they don’t throw CME and may reflect some updates during iteration. That’s often exactly what you want for monitoring, caching, or best-effort reporting—but it’s not what you want for strict transactional logic.\n\nMy practical advice: when correctness requires a consistent snapshot, take one explicitly (copy keys/entries to a list and iterate that).\n\n## Practical Scenarios Where for-each Shines\nThe best way to build intuition is to look at \“real work\” loops—things that show up in services, CLI tools, and batch jobs.\n\n### Scenario 1: Aggregation with multiple counters\nFor-each is great when you’re collecting stats because it avoids index noise and stays easy to extend.\n\n import java.util.List;\n\n class Metrics {\n static class Summary {\n final int total;\n final int blanks;\n final int invalid;\n\n Summary(int total, int blanks, int invalid) {\n this.total = total;\n this.blanks = blanks;\n this.invalid = invalid;\n }\n\n @Override\n public String toString() {\n return "Summary{total=" + total + ", blanks=" + blanks + ", invalid=" + invalid + "}";\n }\n }\n\n static Summary summarizeSkus(List skus) {\n int blanks = 0;\n int invalid = 0;\n\n for (String sku : skus) {\n if (sku == null sku.isBlank()) {\n blanks++;\n continue;\n }\n if (!sku.matches("[A-Z]{3}-\\\\d{4}")) {\n invalid++;\n }\n }\n\n return new Summary(skus.size(), blanks, invalid);\n }\n }\n\nI like how easy it is to add one more counter without rewriting a pipeline or introducing intermediate structures.\n\n### Scenario 2: Early exit on first failure\nIf the moment you find a failure you can stop, a loop is usually the cleanest expression of that idea.\n\n class AnyInvalid {\n static boolean anyInvalid(Iterable skus) {\n for (String sku : skus) {\n if (sku == null

sku.isBlank()) {\n return true;\n }\n if (!sku.matches("[A-Z]{3}-\\\\d{4}")) {\n return true;\n }\n }\n return false;\n }\n }\n\nYes, streams can do anyMatch, but when the predicate isn’t a single clean expression (and you want logging or special cases), for-each keeps it readable.\n\n### Scenario 3: Processing with backpressure-like batching\nIf you need to accumulate a batch and flush periodically (common in database writes or API calls), for-each is straightforward.\n\n import java.util.ArrayList;\n import java.util.List;\n\n class BatchProcessor {\n static void processInBatches(List items, int batchSize) {\n List batch = new ArrayList(batchSize);\n\n for (String item : items) {\n batch.add(item);\n if (batch.size() == batchSize) {\n flush(batch);\n batch.clear();\n }\n }\n\n if (!batch.isEmpty()) {\n flush(batch);\n }\n }\n\n static void flush(List batch) {\n // send batch somewhere\n }\n }\n\nThis is the kind of loop that becomes awkward if you force it into a functional pipeline.\n\n## When for-each Backfires (and What I Use Instead)\nThe for-each loop isn’t a hammer for every nail. Here are the cases where I routinely switch tools.\n\n### 1) In-place replacement\nIf you need to replace elements (not mutate their internal state), use an index loop for arrays and lists.\n\nExample: normalize and store back into a String[]:\n\n class ReplaceInArray {\n static void normalizeInPlace(String[] arr) {\n for (int i = 0; i < arr.length; i++) {\n String s = arr[i];\n arr[i] = (s == null) ? null : s.trim().toLowerCase();\n }\n }\n }\n\n### 2) Zipping / pairing two structures\nIf you need to walk two lists together by position (zip), for-each is the wrong tool because it hides the index.\n\n import java.util.List;\n\n class Zip {\n static void printPairs(List a, List b) {\n int n = Math.min(a.size(), b.size());\n for (int i = 0; i < n; i++) {\n System.out.println(a.get(i) + " / " + b.get(i));\n }\n }\n }\n\n### 3) Reverse iteration\nIf order is important and you need to go backwards, just use an index loop (or ListIterator if you need iterator semantics).\n\n for (int i = list.size() – 1; i >= 0; i–) {\n var item = list.get(i);\n // …\n }\n\n### 4) Removing while iterating with complex state\nremoveIf is great for pure predicates. But if the removal decision depends on evolving state (like a rolling window), explicit iterators are clearer and safer.\n\n## A Quick Debugging Trick: Turn for-each into Iterator Form in Your Head\nWhen debugging a tricky loop, I mentally \“desugar\” it into iterator form (for collections). It makes questions like these easier:\n\n- \“Am I modifying the collection during iteration?\”\n- \“What exactly defines the iteration order?\”\n- \“Is this a snapshot iterator or a live iterator?\”\n\nIf I suspect a CME or ordering issue, I rewrite it explicitly in my head (or temporarily in code) to see where the iterator is coming from and what might be touching the collection.\n\n## The Style Rules I Actually Enforce in Reviews\nI don’t enforce style for its own sake. I enforce it because loops are where bugs hide. These are my practical rules.\n\n1) Prefer for-each for traversal by default\n- If the index doesn’t matter, don’t invent one.\n\n2) Name the loop variable like a real domain concept\n- for (Order order : orders) beats for (var o : orders) when the type is not obvious.\n\n3) Keep the loop body narrow\n- If you’re nesting conditionals, consider early continue or extracting helper methods.\n\n4) Don’t mutate containers during for-each\n- Use removeIf, explicit iterators, or a two-pass approach.\n\n5) Make ordering explicit when it matters\n- Choose the right collection type or sort before iteration.\n\n6) If it’s hot, measure\n- Don’t guess about performance. Profile, then decide whether you need arrays, primitives, or a different structure.\n\n## Closing Thoughts\nFor-each loops are one of those features that feel \“basic\” until you’ve lived through enough production bugs to appreciate what they remove: unnecessary state, unnecessary math, and unnecessary opportunities to be wrong. I use them because they communicate intent clearly: \“I am going to process each element.\”\n\nAt the same time, I treat for-each as a tool with crisp boundaries. The moment I need index semantics, reverse traversal, in-place replacement, or complex structural mutation, I switch to the tool that makes those constraints explicit. That’s the real skill: not just knowing the syntax, but choosing the loop form that makes correctness obvious to the next person reading the code—often future me.

Scroll to Top