The ArrayList forEach() method runs a given action once for every element of the list, in index order, and returns nothing. The action is a Consumer, which means the loop body becomes a lambda expression or a method reference.
We use forEach() when each element needs the same side effect, such as printing, logging, sending a message or adding to another collection. It is not meant for computing a result, which is the job of a stream.
The following example copies city names into a second list and prints them.
List<String> cities = new ArrayList<>(List.of("Oslo", "Rome", "Lima"));
List<String> labels = new ArrayList<>();
cities.forEach(c -> labels.add(c.toUpperCase()));
List<String> result = labels; // [OSLO, ROME, LIMA]
cities.forEach(System.out::println); // prints Oslo, Rome, Lima on three lines
Notice that forEach() itself returns void, so the lambda writes into labels as a side effect. We cover what the action can be, how to reuse and chain actions, the four things forEach() cannot do and their replacements, changing the list during the call, maps, and the stream versions of forEach().
1. The Consumer Behind forEach()
The method comes from Iterable as default void forEach(Consumer<? super T> action). A Consumer is a functional interface with one method, accept(T), which takes an element and returns nothing. The ArrayList class overrides the default with a plain loop over its backing array.

A few rules follow from that design, and the null cases surprise people most.
- The elements arrive in index order, on the calling thread.
- A null action throws NullPointerException before the first element.
- A null element is passed to the action like any other value.
List<String> names = new ArrayList<>(Arrays.asList("Ana", null, "Ben"));
List<String> seen = new ArrayList<>();
names.forEach(n -> seen.add(String.valueOf(n)));
List<String> visited = seen; // [Ana, null, Ben]
names.forEach(null); // NullPointerException
A lambda such as n -> n.length() would throw on the null element, so actions on lists with gaps check for null first, or the list is cleaned with removeIf(Objects::isNull) beforehand.
2. Lambdas and Method References as the Action
A lambda is the most flexible action. A method reference is shorter when an existing method already takes one element, and three kinds of method references fit a Consumer.
class Tracker {
static final List<String> AUDIT = new ArrayList<>();
static void audit(String city) {
AUDIT.add("visited " + city);
}
}
record Stop(String city) {
void announce() {
Stop.SIGN.add("Next stop " + city);
}
static final List<String> SIGN = new ArrayList<>();
}
List<String> cities = List.of("Oslo", "Rome");
List<String> copy = new ArrayList<>();
cities.forEach(Tracker::audit); // static method
cities.forEach(copy::add); // method of one particular object
List<Stop> stops = List.of(new Stop("Oslo"), new Stop("Rome"));
stops.forEach(Stop::announce); // method of each element
List<String> audit = Tracker.AUDIT; // [visited Oslo, visited Rome]
List<String> copied = copy; // [Oslo, Rome]
List<String> board = Stop.SIGN; // [Next stop Oslo, Next stop Rome]
A method reference to a method that returns a value, such as copy::add, which returns a boolean, still works because a Consumer ignores the return value.
A lambda with several statements opens a block with braces. Once the block grows past a few lines, a named method and a method reference read better and are easier to test.
List<String> cities = List.of("Oslo", "Rome", "Lima");
StringBuilder csv = new StringBuilder();
cities.forEach(c -> {
if (!csv.isEmpty()) {
csv.append(',');
}
csv.append(c.toLowerCase());
});
String line = csv.toString(); // "oslo,rome,lima"
3. Reusing and Chaining Actions With andThen()
An action stored in a Consumer variable can be passed to several forEach() calls. The default method andThen() joins two consumers into one, which runs the first action and the second action for each element before moving to the next element.
List<String> log = new ArrayList<>();
List<String> outbox = new ArrayList<>();
Consumer<String> logIt = c -> log.add("log " + c);
Consumer<String> queueIt = c -> outbox.add("mail " + c);
List.of("Oslo", "Rome").forEach(logIt.andThen(queueIt));
List<String> logged = log; // [log Oslo, log Rome]
List<String> queued = outbox; // [mail Oslo, mail Rome]
The chained version walks the list once, whereas two separate forEach() calls walk it twice. When the first action throws for an element, the second action does not run for that element, and the loop stops.
4. What forEach() Cannot Do and the Replacements
The action is a lambda, not a loop body, so four things that work in a for loop do not work inside forEach(). Each one has a direct replacement.
4.1. Leaving Before the Last Element
A lambda cannot use break, and return inside it only ends the call for the current element. When the loop must end at the first match, a stream with anyMatch() or takeWhile() does the job, and so does a plain for-each loop with break.
List<Integer> temps = List.of(18, 21, 35, 19);
List<Integer> printed = new ArrayList<>();
temps.forEach(t -> {
if (t > 30) {
return; // skips 35 only, the loop goes on
}
printed.add(t);
});
List<Integer> skipped = printed; // [18, 21, 19]
List<Integer> untilHot = temps.stream().takeWhile(t -> t <= 30).toList(); // [18, 21]
boolean anyHot = temps.stream().anyMatch(t -> t > 30); // true
4.2. Getting the Index
The Consumer receives only the element. When the position matters, for example for numbered lines, we stream the indexes with IntStream.range(), which reads each element with the constant-time get(i) of an ArrayList. The tutorial on iterating with indices has more options.
List<String> cities = List.of("Oslo", "Rome", "Lima");
List<String> numbered = new ArrayList<>();
IntStream.range(0, cities.size()).forEach(i -> numbered.add((i + 1) + ". " + cities.get(i)));
List<String> lines = numbered; // [1. Oslo, 2. Rome, 3. Lima]
4.3. Throwing Checked Exceptions
The accept() method declares no checked exceptions, so a call such as Files.writeString() inside the lambda does not compile unless we catch IOException. We catch it and rethrow it as UncheckedIOException, which keeps the original exception as the cause.
// does not compile: unreported exception IOException
cities.forEach(c -> Files.writeString(Path.of(c + ".txt"), c));
static void writeFile(Path dir, String city) {
try {
Files.writeString(dir.resolve(city + ".txt"), city);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
Path dir = Files.createTempDirectory("cities");
List.of("Oslo", "Rome").forEach(c -> writeFile(dir, c));
int files = dir.toFile().list().length; // 2
A helper method keeps the lambda on one line and gives the error handling a name. The exception handling in streams article shows other ways, such as collecting failures instead of stopping.
4.4. Updating a Local Variable
A lambda can read local variables only when they are effectively final, so count++ on a local int does not compile. For a count or a sum, a stream returns the value as its result. When we still need a side counter, an AtomicInteger works because the reference stays final while its value changes.
// does not compile: local variables referenced from a lambda must be final or effectively final
int count = 0;
cities.forEach(c -> count++);
List<String> cities = List.of("Oslo", "Rome", "Lima");
long longNames = cities.stream().filter(c -> c.length() > 3).count(); // 3
AtomicInteger letters = new AtomicInteger();
cities.forEach(c -> letters.addAndGet(c.length()));
int totalLetters = letters.get(); // 12
5. Changing the List Inside forEach()
The action may change the elements, but adding or removing elements of the same list during the iteration is not allowed. A call to set() replaces a value without changing the size, so it works. Adding or removing changes the modification count of the ArrayList, and forEach() throws ConcurrentModificationException.
List<String> codes = new ArrayList<>(List.of("osl", "fco", "lim"));
List<String> view = codes;
codes.forEach(c -> view.set(view.indexOf(c), c.toUpperCase()));
List<String> upper = codes; // [OSL, FCO, LIM]
The indexOf() trick breaks on duplicate values and scans the list for every element. The method replaceAll() is made for the job and takes a function that returns the new value.
List<String> codes = new ArrayList<>(List.of("osl", "fco", "lim"));
codes.replaceAll(String::toUpperCase);
List<String> replaced = codes; // [OSL, FCO, LIM]
A structural change is worse than it looks, because the exception arrives after the change. The ArrayList checks the count on each step and once more at the end, so the element is already gone when the exception reaches us.
List<String> trip = new ArrayList<>(List.of("Oslo", "Rome", "Lima"));
trip.forEach(c -> { if (c.equals("Oslo")) trip.remove("Lima"); }); // ConcurrentModificationException
List<String> afterError = trip; // [Oslo, Rome]
To delete elements by a rule, we call removeIf() instead, which is explained in the ArrayList removeIf() guide.
6. Passing a BiConsumer to Map.forEach()
A Map is not an Iterable, but it has its own forEach() that takes a BiConsumer with the key and the value. That avoids calling getKey() and getValue() on each entry.
Map<String, Integer> visitors = new TreeMap<>(Map.of("Rome", 120, "Oslo", 45));
List<String> report = new ArrayList<>();
visitors.forEach((city, count) -> report.add(city + "=" + count));
List<String> rows = report; // [Oslo=45, Rome=120]
We used a TreeMap so the keys come out sorted. A HashMap visits its entries in an order that depends on the hash codes and can change between JDK versions, so its output order is never something to rely on.
7. list.forEach() vs stream().forEach() vs forEachOrdered()
A stream also has a forEach() method, and it behaves differently once the stream runs in parallel. The List version always follows the index order on the calling thread.
| Call | Order | Threads | Typical use |
|---|---|---|---|
| list.forEach(action) | Index order | Calling thread | One action per element |
| list.stream().forEach(action) | Index order for a sequential stream | Calling thread | After filter() or map() |
| list.parallelStream().forEach(action) | No order guarantee | Several threads | Independent, CPU-heavy work |
| list.parallelStream().forEachOrdered(action) | Index order | Several threads, ordered output | Parallel work with ordered results |
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
List<Integer> ordered = new ArrayList<>();
nums.parallelStream().map(n -> n * 10).forEachOrdered(ordered::add);
List<Integer> tens = ordered; // [10, 20, 30, 40, 50, 60]
With forEach() instead of forEachOrdered(), the parallel version adds the numbers in any order, and adding to a plain ArrayList from several threads can also lose elements. For a result list, toList() on the stream is the safe choice.
8. Sending Shipping Notifications
Say an online shop marks orders as shipped every hour and sends one email per order. A failure for one address must not stop the other emails, and the job reports the addresses that failed. The forEach() action calls the mail service and catches the error per element.
interface Mailer {
void send(String to, String text);
}
static List<String> notifyShipped(List<String> emails, Mailer mailer) {
List<String> failed = new ArrayList<>();
emails.forEach(to -> {
try {
mailer.send(to, "Your order has shipped");
} catch (IllegalArgumentException e) {
failed.add(to);
}
});
return failed;
}
List<String> sent = new ArrayList<>();
Mailer fake = (to, text) -> {
if (!to.contains("@")) {
throw new IllegalArgumentException("bad address " + to);
}
sent.add(to);
};
List<String> failures = notifyShipped(List.of("ana@shop.io", "ben-at-shop", "eva@shop.io"), fake); // [ben-at-shop]
int delivered = sent.size(); // 2
The test passes a fake Mailer written as a lambda, so no real email leaves the machine. For the wider picture of every loop style, including index loops and iterators, see iterating through an ArrayList.
9. ArrayList forEach() FAQs
Teams that replace classic loops with forEach() run into four recurring questions.
9.1. Does forEach() run in parallel?
No. The List.forEach() method runs on the calling thread in index order. Only parallelStream().forEach() uses several threads, as the table in section 7 shows.
9.2. Can forEach() return a value?
No, it returns void. To build a new list or a total, we use a stream, for example cities.stream().map(String::length).toList(), instead of filling a list from inside the action.
9.3. Can we call forEach() on an array?
No, because arrays do not implement Iterable. We wrap the array with Arrays.asList() or stream it with Arrays.stream().
String[] codes = {"osl", "fco"};
List<String> seenCodes = new ArrayList<>();
Arrays.stream(codes).forEach(seenCodes::add);
List<String> fromArray = seenCodes; // [osl, fco]
9.4. Is forEach() slower than a for loop?
For an ArrayList, the difference is too small to matter in normal code, because both read the same backing array. The lambda adds one method call per element, which the JIT compiler can inline. We choose by readability and measure with a tool such as JMH only when profiling shows the loop as a hot spot.
10. Conclusion
The forEach() method hands every element of an ArrayList to a Consumer in index order. Method references keep simple actions short, and andThen() combines two actions in one pass.
Inside the action, we cannot break, read an index, throw checked exceptions or change local variables, so we switch to takeWhile() or a loop, IntStream.range(), a wrapping helper and a stream result. We change elements with replaceAll() and remove them with removeIf(), never with add() or remove() inside the action. The ArrayList guide covers the rest of the list API.
11. References
- Iterable.forEach() (Java SE 25 API)
- ArrayList.forEach() (Java SE 25 API)
- Consumer (Java SE 25 API)
- Stream.forEachOrdered() (Java SE 25 API)
Happy Learning !!