The simplest way to iterate an ArrayList in Java is the enhanced for loop, for (String item : list), which visits every element in index order. When we also need the index, we use a classic for loop with get(i), and when we want to pass the work as a lambda, we call forEach().
Looping through an ArrayList is how we print a report, sum up prices, validate imported rows or build a response from database results. The choice of loop matters once we need the position of an element, need to remove elements on the way, or want to walk the list backwards.
The following example walks a list of recipe steps in five different ways.
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
for (String step : steps) { // chop, fry, plate
System.out.println(step);
}
for (int i = 0; i < steps.size(); i++) { // 1. chop, 2. fry, 3. plate
System.out.println((i + 1) + ". " + steps.get(i));
}
steps.forEach(System.out::println); // chop, fry, plate
steps.reversed().forEach(System.out::println); // plate, fry, chop (Java 21)
steps.removeIf(step -> step.startsWith("f")); // removes "fry" safely
List<String> remaining = steps; // [chop, plate]
Notice that only removeIf() changes the list, and none of the lines needs an Iterator object in our code. We go through each style in turn, see why removing elements inside a loop throws ConcurrentModificationException, and finish with a table that matches each task to a loop.
1. Iterating With the Enhanced for Loop
The enhanced for loop (also called the for-each loop) is the default choice for reading every element. The compiler turns it into an Iterator call behind the scenes, so it works on any Iterable, not only on lists, and it never goes out of bounds.
List<String> steps = List.of("chop", "fry", "plate");
StringBuilder card = new StringBuilder();
for (String step : steps) {
card.append(step).append(" > ");
}
String recipe = card.toString(); // "chop > fry > plate > "
The loop variable is a copy of the reference, so assigning a new value to step inside the loop does not change the list. To replace elements, we use an index with set() or call replaceAll(), as the next sections show.
2. Using an Index With the Classic for Loop
A classic for loop gives us the position of each element, which we need for numbering lines, comparing neighbors or replacing values with set(). Calling get(i) on an ArrayList is a constant-time array access, so an index loop is as fast as the for-each loop.
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
for (int i = 0; i < steps.size(); i++) {
steps.set(i, (i + 1) + ". " + steps.get(i));
}
List<String> numbered = steps; // [1. chop, 2. fry, 3. plate]
A while loop with a counter does the same job and appears in older code. It is longer and easier to get wrong, because the counter lives outside the loop and we must increment it ourselves.
List<String> steps = List.of("chop", "fry", "plate");
int index = 0;
int letters = 0;
while (index < steps.size()) {
letters += steps.get(index++).length();
}
int totalLetters = letters; // 12
The index loop is the right tool for an ArrayList, but not for a LinkedList, where each get(i) walks the nodes from one end. On a LinkedList, we use the for-each loop or an iterator instead.
3. The forEach() Method With a Lambda
The forEach() method comes from the Iterable interface (Java 8) and runs a Consumer action for every element in iteration order. It takes a lambda expression or a method reference, so the loop fits on one line.
List<String> steps = List.of("Chop", "Fry", "Plate");
List<String> lower = new ArrayList<>();
steps.forEach(step -> lower.add(step.toLowerCase()));
List<String> result = lower; // [chop, fry, plate]
We can also keep the action in a variable and reuse it, or open a block with braces when the action needs more than one statement.
List<String> steps = List.of("chop", "fry", "plate");
List<String> log = new ArrayList<>();
Consumer<String> logStep = step -> log.add("step: " + step);
steps.forEach(logStep);
steps.forEach(step -> {
String upper = step.toUpperCase();
log.add(upper);
});
int entries = log.size(); // 6
3.1. How ArrayList.forEach() Checks for Changes
The ArrayList class overrides forEach() with a plain loop over its internal array. Before the loop, it saves the list’s modification count, and after the loop it compares the count again. If the action added or removed an element, the method throws ConcurrentModificationException once the loop ends, so the change has already happened when we see the exception.
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
steps.forEach(step -> steps.add("wash")); // ConcurrentModificationException
A null action throws NullPointerException before the loop starts. The lambda also cannot assign to local variables of the enclosing method, because they must be effectively final, so counting with forEach() needs a field, an array or a stream with count().
3.2. forEach() vs the for-each Loop
Both forms visit the same elements in the same order. The differences show up in control flow and in what the body can touch.
| Feature | for-each loop | forEach() method |
|---|---|---|
| break and continue | Allowed | Not allowed; return skips only the current element |
| Checked exceptions in the body | Allowed if the method declares them | Must be caught inside the lambda |
| Assign to local variables | Allowed | Not allowed (effectively final rule) |
| Works on arrays | Yes | No, only on Iterable types |
| Typical use | Logic with conditions and early exit | One action per element, such as logging or sending |
4. Iterator and ListIterator
An Iterator is the object the for-each loop uses in the background. We create it ourselves when we need remove() during the loop. A ListIterator adds previous(), set(), add() and the current index, so it can walk backwards and edit the list in place.
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
Iterator<String> it = steps.iterator();
while (it.hasNext()) {
if (it.next().equals("fry")) {
it.remove();
}
}
List<String> left = steps; // [chop, plate]
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
ListIterator<String> li = steps.listIterator(steps.size());
StringBuilder backwards = new StringBuilder();
while (li.hasPrevious()) {
int pos = li.previousIndex();
String step = li.previous();
backwards.append(pos).append('=').append(step).append(' ');
li.set(step.toUpperCase());
}
String visited = backwards.toString().strip(); // "2=plate 1=fry 0=chop"
List<String> edited = steps; // [CHOP, FRY, PLATE]
The legacy Enumeration interface does the same job as Iterator for Vector and Hashtable. Modern code has no reason to use it, because it has no remove() and every collection already returns an Iterator.
5. Removing Elements During Iteration
The iterators of ArrayList are fail-fast. If the list changes structurally while we loop (an element is added or removed through the list instead of the iterator), the next call to next() throws ConcurrentModificationException. A for-each loop hides the iterator, so calling list.remove() inside it is the most common cause of the exception.
static void dropStep(List<String> steps, String name) {
for (String step : steps) {
if (step.equals(name)) {
steps.remove(step); // changes the list inside the loop
}
}
}
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
dropStep(steps, "chop"); // ConcurrentModificationException
Since Java 8, removeIf() is the shortest safe fix. It takes a condition, removes every matching element in one pass and returns true if anything was removed. For more ways to delete elements, see removing elements from an ArrayList.
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate", "fry again"));
boolean changed = steps.removeIf(step -> step.startsWith("fry")); // true
List<String> left = steps; // [chop, plate]
When another thread may change the list while we read it, neither fix helps. In that case, we iterate a CopyOnWriteArrayList, whose iterator works on a snapshot, or we copy the list with List.copyOf() while holding the same lock that the writing threads use.
6. Looping in Reverse With reversed()
Java 21 added sequenced collections, which gave List the reversed() method. It returns a reverse-ordered view of the same list without copying, so any loop style works on it.
List<String> steps = new ArrayList<>(List.of("chop", "fry", "plate"));
List<String> undo = new ArrayList<>();
for (String step : steps.reversed()) {
undo.add("undo " + step);
}
List<String> undoOrder = undo; // [undo plate, undo fry, undo chop]
String last = steps.getLast(); // "plate"
Before Java 21, we used a ListIterator from the end, as in section 4, or a classic for loop counting down from size() – 1. The reversed() view is shorter and leaves no room for an off-by-one error.
7. Iterating With Streams
A stream is the better fit when the loop filters or transforms elements and collects a result. The stream() method processes the elements in list order, and a terminal operation such as toList() or count() produces the result. See Java Streams by examples for the full API.
List<String> steps = List.of("chop", "fry", "plate");
List<String> longSteps = steps.stream()
.filter(step -> step.length() > 3)
.map(String::toUpperCase)
.toList(); // [CHOP, PLATE]
long count = steps.stream().filter(s -> s.contains("p")).count(); // 2
Streams have no built-in index. When we need one, we stream the indexes with IntStream.range() and read each element with get(), which is cheap on an ArrayList. More options are in iterating a stream with indices.
List<String> steps = List.of("chop", "fry", "plate");
List<String> lines = IntStream.range(0, steps.size())
.mapToObj(i -> (i + 1) + ". " + steps.get(i))
.toList(); // [1. chop, 2. fry, 3. plate]
A parallelStream().forEach() call does not keep the list order, and its action runs on several threads. We use it only for independent, CPU-heavy work on large lists, and use forEachOrdered() when the order matters.
8. Which Loop Fits Which Task
Every style reads the same elements, so the choice depends on what else the loop must do. The decision mostly comes down to two questions, namely whether we need the index and whether the loop changes the list.

| Task | Best choice | Why |
|---|---|---|
| Read every element | for-each loop | Shortest, no index errors |
| Need the position or replace values | Classic for loop with get(i)/set(i) | Constant-time index access on ArrayList |
| One action per element | forEach() | One line with a method reference |
| Remove by condition | removeIf() | Safe, one pass |
| Remove or edit with custom logic | Iterator / ListIterator | remove(), set(), add() during the loop |
| Walk backwards | reversed() (Java 21) | A view, no copy |
| Filter, transform and collect | stream() | Result in one expression |
9. Printing Numbered Recipe Steps
Say a recipe app stores the cooking steps of a dish in an ArrayList. The recipe page shows the steps numbered from 1, and the shopping mode hides the optional steps, such as garnishing. Both views come from the same list.
record Step(String text, boolean optional) {}
List<Step> recipe = new ArrayList<>(List.of(
new Step("chop onions", false),
new Step("fry onions", false),
new Step("add parsley", true),
new Step("plate", false)));
List<String> page = new ArrayList<>();
for (int i = 0; i < recipe.size(); i++) {
page.add((i + 1) + ". " + recipe.get(i).text());
}
String firstLine = page.getFirst(); // "1. chop onions"
recipe.removeIf(Step::optional);
List<String> quickMode = recipe.stream().map(Step::text).toList(); // [chop onions, fry onions, plate]
The numbered page needs the index, so it uses a classic for loop. The quick mode removes the optional steps with removeIf() and maps the rest with a stream, and no loop body touches the list while it is being iterated.
10. ArrayList Iteration FAQs
Developers who loop through lists ask about speed, indexes and removal.
10.1. What is the fastest way to iterate an ArrayList?
For an ArrayList, the for-each loop, the index loop and forEach() all read the same backing array, so the differences are too small to matter in normal code. We pick the most readable form and measure with a benchmark tool such as JMH only when a profiler shows the loop as a hot spot.
10.2. How do we get the index inside forEach()?
The forEach() method has no index parameter. We use a classic for loop, or IntStream.range(0, list.size()).forEach(i -> …) when we want to keep the lambda style.
10.3. Why does removing the second-to-last element not throw ConcurrentModificationException?
After that removal, the list is one element shorter, so hasNext() returns false and the loop ends before next() can detect the change. The last element is skipped without any error, which makes the bug harder to find, so we never call list.remove() inside a for-each loop.
10.4. Can forEach() modify the elements of the list?
Partly. The method can change the state of mutable elements, but it cannot add or remove elements without throwing ConcurrentModificationException. To replace every element with a new value, we call replaceAll(), for example steps.replaceAll(String::toUpperCase).
10.5. How do we iterate over a List of Lists?
We nest two for-each loops, or flatten the lists with stream().flatMap(List::stream) when we need one sequence of elements. The same loop styles also work for other collection types, as shown in iterating over collections.
11. Conclusion
For plain reading, the enhanced for loop is the clearest way to iterate an ArrayList. The classic index loop is the tool for positions and in-place replacement, and forEach() fits one action per element.
When elements must go, we use removeIf() or Iterator.remove() instead of list.remove() inside a loop, because the fail-fast iterator throws ConcurrentModificationException. Since Java 21, reversed() handles backward loops, and streams cover filtering and collecting in one expression.
12. References
- ArrayList Javadoc (Java 25)
- Iterable.forEach() Javadoc
- ListIterator Javadoc
- JEP 431, Sequenced Collections
- ConcurrentModificationException Javadoc
Happy Learning !!
Its very useful to new learning people and the syntax also easy to understood who getting difficult to understand.