Back to Blog
Java

Java ArrayList foreach: Syntax, Pitfalls, and Performance

java arraylist foreach: Learn how to iterate an ArrayList with the foreach loop in Java, including iterator behavior, safe removal, performance tradeoffs, and stream a...

JavaArrayListforeachEnhanced For LoopIteratorStreams
Illustration of an ArrayList with elements being traversed by a foreach loop arrow, showing a cursor moving through the list.

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 for java arraylist foreach:

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 desugars to an iterator-based loop.

How the Enhanced For Loop Works with ArrayList

The Java 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); }

For ArrayList, the iterator() method returns a ListIterator that tracks a cursor position. Each call to next() moves the cursor forward and returns the element at that index. The iterator is fail-fast: if the list is structurally modified (elements added or removed) after the iterator is created, the iterator throws ConcurrentModificationException on the next access.

This behavior is intentional. It prevents undefined iteration state when the underlying list changes. The modCount field of ArrayList is compared to the iterator's expected modCount; any discrepancy triggers the exception.

Modifying Elements During Iteration

Setting an existing element's value is not a structural modification, so it is safe inside a foreach loop:

for (String name : names) { if (name.equals("Bob")) { // This is fine; it replaces the element at the current index // But we don't have the index here, so we need a different approach. } }

Actually, the foreach loop does not expose the index. To replace an element, you need an indexed loop or an ListIterator that supports set(). The foreach loop is read-only in terms of structure; you can call methods on the elements themselves, but you cannot add or remove elements from the list without causing a ConcurrentModificationException.

Consider this common mistake:

for (String name : names) { if (name.equals("Bob")) { names.remove(name); // Throws ConcurrentModificationException } }

Removing an element changes the list's size and shifts subsequent elements left. The iterator's cursor becomes invalid, and the next hasNext() or next() call throws. The same 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 exception occurs. This is the only safe way to remove during iteration without collecting the elements first.

Alternatively, you can collect the elements to remove and then remove them 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 adds memory overhead and a second pass. The Iterator.remove() approach is more efficient and idiomatic.

Performance: foreach vs Indexed Loop vs Streams

For an ArrayList, the enhanced for loop and an indexed for loop have nearly identical performance because the iterator uses the same underlying get(index) mechanism. The iterator adds a small amount of method-call overhead, but in practice the JIT compiler often inlines it.

// Indexed loop for (int i = 0; i < names.size(); i++) { String name = names.get(i); }

Both approaches are O(n) and have similar constant factors. The indexed loop gives you the index, which is useful when you need it. The foreach loop is more concise and works with any Iterable, so it is preferred unless index access is required.

Streams provide another iteration style:

names.stream().forEach(name -> System.out.println(name));

Streams introduce additional abstraction and can be slower for simple iteration, especially with small lists, because they create a pipeline and may allocate intermediate objects. However, streams shine when you combine operations like filtering, mapping, and collecting. For raw iteration, the foreach loop is usually faster and clearer.

The table below summarizes the tradeoffs:

ApproachIndex accessSafe removalReadabilityOverhead
Enhanced forNoNo (directly)HighLow
Indexed forYesNo (directly)MediumLow
Iterator whileNoYes (via remove)MediumLow
Stream forEachNoNo (directly)MediumModerate

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 is more expressive than a foreach loop with an if inside. However, Stream.forEach() does not guarantee encounter order for parallel streams, and it does not allow you to break out of the loop early. The enhanced for loop is imperative and supports break and return directly.

If you need to stop iterating when a condition is met, the foreach loop is simpler:

for (String name : names) { if (name.equals("Bob")) { break; } }

With streams, you would need anyMatch() or a custom Spliterator, which is more complex. Choose streams when you are building a pipeline; choose foreach for straightforward iteration.

Edge Cases and Compatibility

The foreach loop handles empty lists gracefully—it simply does not execute. It also works with lists containing null elements, 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()); } }

One subtle compatibility point: the enhanced for loop requires the list to implement Iterable, which ArrayList does. If you are using an older Java version (pre-5), the enhanced for loop is not available, but that is rarely relevant today. The iterator-based code remains the same across modern Java versions, and the fail-fast behavior has been stable since Java 2.

Another edge case is modifying the list from another thread while iterating. The fail-fast iterator is not thread-safe; it throws ConcurrentModificationException if the list is modified concurrently. For concurrent access, use CopyOnWriteArrayList or synchronize externally.

When to Choose a Different Collection Type

The foreach loop works with any List, but ArrayList is not always the best choice for iteration-heavy workloads. If you frequently insert or remove elements at the beginning, a LinkedList may be better, though its iterator has similar fail-fast behavior. If you need to iterate while another thread modifies the list, CopyOnWriteArrayList provides a snapshot iterator that does not throw, but it copies the entire array on every write.

For most read-only iteration scenarios, ArrayList is the default and works well with the foreach loop. The decision to use another collection should be based on the mutation pattern, not the iteration syntax.

The enhanced for loop over an ArrayList is a fundamental Java skill. Understanding its iterator behavior, safe removal patterns, and performance characteristics prevents subtle bugs and keeps your code efficient. When you need to remove elements, use Iterator.remove(); when you need a functional pipeline, consider streams; otherwise, the foreach loop is the clearest and most maintainable choice.

java arraylist foreach: Practical Usage and Code Examples | RYUSLOG DEV