Java ArrayList foreach: Syntax, Pitfalls, and Performance
Learn how to iterate over a Java ArrayList with the foreach loop, including iterator behavior, safe removal, performance tradeoffs, and stream alternatives.
The foreach loop (officially the enhanced for loop) is the most common way to iterate over an ArrayList in Java. It reads cleanly, avoids explicit index management, and works with any Iterable. But its apparent simplicity hides how it interacts with the list's iterator, which matters when you modify the list during iteration.
Here is the basic syntax:
ArrayList<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); names.add("Carol"); for (String name : names) { System.out.println(name); }
The loop variable name receives each element in order, from index 0 to size() - 1. This works because ArrayList implements Iterable, and the enhanced for loop is compiled to an iterator-based loop.
How the Enhanced For Loop Works with ArrayList
For a class that implements Iterable, the compiler translates the enhanced for loop into a while loop using an Iterator:
for (Iterator<String> it = names.iterator(); it.hasNext(); ) { String name = it.next(); System.out.println(name); }
ArrayList.iterator() returns an Iterator that tracks a cursor position. Each call to next() moves the cursor forward and returns the element at that position. The separate listIterator() method returns a ListIterator, which adds set() and add() operations.
This iterator is fail-fast: if the list is structurally modified after the iterator is created, the iterator may throw ConcurrentModificationException on a later use. The check is best-effort, not a guaranteed concurrency protection. The ArrayList implementation keeps a modCount counter, and the iterator stores the expected modCount. A mismatch triggers the exception when certain iterator operations are performed.
Modifying Elements During Iteration
The enhanced for loop gives you a reference to each current element. You can call methods on that object, and if the object is mutable, you can change its internal state. However, the loop does not expose the index, so you cannot replace the element at that position in the ArrayList directly. For replacement, use an indexed loop or a ListIterator:
for (int i = 0; i < names.size(); i++) { if ("Bob".equals(names.get(i))) { names.set(i, "Robert"); } }
or:
ListIterator<String> it = names.listIterator(); while (it.hasNext()) { if ("Bob".equals(it.next())) { it.set("Robert"); } }
Adding or removing elements from the list directly inside the enhanced for loop is a structural modification and is unsafe. This common mistake can fail unpredictably:
for (String name : names) { if (name.equals("Bob")) { names.remove(name); } }
Because ArrayList's iterator is fail-fast, this code may throw ConcurrentModificationException. The exception is not guaranteed: a removal that happens to make the iterator's cursor equal to the new size can end the loop before the mismatch is checked. Do not rely on direct structural changes inside a foreach loop. The same restriction applies to add().
Removing Elements Safely
If you need to remove elements while iterating, use an explicit Iterator and its remove() method:
Iterator<String> it = names.iterator(); while (it.hasNext()) { String name = it.next(); if (name.equals("Bob")) { it.remove(); } }
The iterator's remove() method updates both the list and the iterator's internal state, so no ConcurrentModificationException occurs. In Java 8 and later, ArrayList.removeIf is often the shortest safe option:
names.removeIf(name -> name.equals("Bob"));
Alternatively, collect the elements to remove and call removeAll() after the loop:
List<String> toRemove = new ArrayList<>(); for (String name : names) { if (name.equals("Bob")) { toRemove.add(name); } } names.removeAll(toRemove);
This works but uses extra memory and a second pass. Iterator.remove() or removeIf is usually clearer and more efficient.
Performance: foreach vs Indexed Loop vs Streams
For an ArrayList, the enhanced for loop and an indexed for loop are both O(n) and have similar practical performance. The enhanced for loop uses an iterator, which adds a small indirection, but the JIT compiler often optimizes this away. Choose between them based on whether you need the index, not on microbenchmarks.
// Indexed loop for (int i = 0; i < names.size(); i++) { String name = names.get(i); }
Streams provide a different iteration style:
names.stream().forEach(name -> System.out.println(name));
Streams add abstraction and are most useful for multi-step pipelines such as filtering, mapping, and collecting. For simple iteration, an enhanced for loop is usually easier to read. If stream overhead matters for your workload, measure it rather than assuming one is always faster.
| Approach | Index access | Safe removal | Readability | Typical use |
|---|---|---|---|---|
| Enhanced for | No | No | High | Read-only iteration |
| Indexed for | Yes | Manual index adjustment | Medium | When index is needed |
| Iterator while | No | Yes, via remove() | Medium | Removal during iteration |
Stream forEach | No | No | Medium | Functional pipelines |
Using Streams as an Alternative
Java 8 introduced Stream.forEach(), which is often confused with the enhanced for loop. The key difference is that streams are designed for functional pipelines, not just iteration. For example, you can filter and then iterate in one pass:
names.stream() .filter(name -> name.startsWith("A")) .forEach(System.out::println);
This can be more expressive than a foreach loop with an if inside. However, Stream.forEach() is not a loop statement: it does not support break or returning from the containing method, and in parallel streams it does not guarantee encounter order. If you need short-circuiting, the enhanced for loop is simpler:
for (String name : names) { if (name.equals("Bob")) { break; } }
With streams, short-circuiting requires operations such as anyMatch() or a custom Spliterator, which is more complex. Use streams when building a pipeline; use the enhanced for loop for straightforward iteration.
Edge Cases and Compatibility
The enhanced for loop handles empty lists gracefully; the loop body simply never runs. It also works with lists containing null, but you must check for null inside the loop to avoid NullPointerException:
ArrayList<String> list = new ArrayList<>(); list.add(null); for (String s : list) { if (s != null) { System.out.println(s.length()); } }
The enhanced for loop requires the collection to implement Iterable, which ArrayList does. It was introduced in Java 5, so pre-Java 5 code cannot use it, but that is rarely relevant today.
If another thread modifies the list while you are iterating, the iterator is not thread-safe. It may throw ConcurrentModificationException, but the fail-fast mechanism is best-effort and cannot be relied on as a synchronization strategy. For concurrent modification, use CopyOnWriteArrayList or synchronize access externally.
When to Choose a Different Collection Type
The foreach loop works with any List, but ArrayList is not always the best choice for every mutation pattern. If you frequently insert or remove elements near the beginning, a LinkedList may be worth considering, though its iterator has the same fail-fast behavior and random access is slower. If you need to iterate while another thread modifies the list, CopyOnWriteArrayList provides a snapshot iterator that does not throw when the list is changed during iteration, but it copies the underlying array on every write.
For most read-only iteration scenarios, ArrayList is the default choice and works well with the enhanced for loop. The decision to use another collection should be based on the mutation pattern, not on the iteration syntax.
The enhanced for loop over an ArrayList is a fundamental Java skill. Understanding its iterator behavior, safe removal patterns, and stream alternatives prevents subtle bugs and keeps code easy to maintain. When you need to remove elements, use Iterator.remove() or removeIf; when you need a functional pipeline, consider streams; otherwise, the enhanced for loop is usually the clearest choice.