Foreach

July 11, 20264 min readUpdated 8/20/2026

forEach runs a piece of code once per element. It arrived with Java 8 as the functional counterpart to the for-each loop, and choosing between the two is mostly a readability question rather than a technical one.

The method

List<String> names = List.of("Ana", "Bo", "Cy");

class Demo {
    void run(List<String> names) {
        names.forEach(name -> System.out.println(name));   // lambda
        names.forEach(System.out::println);                // method reference — same thing
    }
}

It takes a Consumer — one argument in, nothing out. That shape is the whole story: forEach exists to cause side effects, because it cannot return anything. If you want a result, you want map, filter or collect instead.

It is declared on Iterable, so every collection has it, and separately on Stream and on Map — three different declarations of the same idea, which is why the map version can take two arguments while the others take one.

On lists and sets

class Demo {
    void run() {
        List<String> names = List.of("Ana", "Bo");
        Set<Integer> ids = Set.of(1, 2, 3);

        names.forEach(name -> System.out.println("hello " + name));
        ids.forEach(System.out::println);

        // Order: a List iterates in order. A HashSet does not promise any.
        new TreeSet<>(ids).forEach(System.out::println);   // 1, 2, 3 — sorted
    }
}

On maps — the two-argument version

This is the one that saves the most typing. Map.forEach takes a BiConsumer, handing you the key and the value directly instead of an Entry to unpack:

class Demo {
    void run() {
        Map<String, Integer> stock = Map.of("apples", 10, "pears", 4);

        // The old way
        for (Map.Entry<String, Integer> entry : stock.entrySet()) {
            System.out.println(entry.getKey() + " = " + entry.getValue());
        }

        // The same thing
        stock.forEach((item, count) -> System.out.println(item + " = " + count));
    }
}

On streams

class Demo {
    void run() {
        List<String> names = List.of("Ana", "Bo", "Christopher");

        names.stream()
                .filter(n -> n.length() > 2)
                .map(String::toUpperCase)
                .forEach(System.out::println);      // ANA, CHRISTOPHER

        // forEachOrdered guarantees encounter order even on a parallel stream
        names.parallelStream().forEachOrdered(System.out::println);
    }
}

On a sequential stream forEach already runs in order. On a parallel one it does not, and forEachOrdered is how you ask for order back — at the cost of most of the parallelism, which is usually a sign the stream should not have been parallel.

What it cannot do

Three limitations, and each one is a reason to use a plain loop instead:

class Demo {
    void run() {
        List<String> names = List.of("Ana", "Bo", "Cy");

        // 1. You cannot break out of it. This runs to the end regardless.
        names.forEach(name -> {
            if (name.equals("Bo")) {
                return;                 // skips THIS element only — like `continue`
            }
            System.out.println(name);
        });

        // A loop can stop early:
        for (String name : names) {
            if (name.equals("Bo")) break;
            System.out.println(name);
        }
    }
}

The return above is the mistake people make: it looks like break and behaves like continue, because it returns from the lambda, not from the loop. There is no way to stop a forEach early. If you need that, use a loop — or anyMatch, findFirst or takeWhile, which short-circuit properly.

2. You cannot modify the collection while iterating. Same rule as a for-each loop, same exception:

class Demo {
    void run() {
        List<String> names = new ArrayList<>(List.of("Ana", "Bo"));

        // names.forEach(n -> { if (n.startsWith("B")) names.remove(n); });
        //   -> ConcurrentModificationException

        names.removeIf(n -> n.startsWith("B"));    // the right way
        System.out.println(names);                  // [Ana]
    }
}

3. You cannot mutate a local variable from inside it, because a lambda can only capture effectively-final variables. A counter is the usual thing people reach for, and the answer is not to fight the rule:

class Demo {
    void run() {
        List<String> names = List.of("Ana", "Bo", "Christopher");

        // int count = 0;
        // names.forEach(n -> { if (n.length() > 2) count++; });   // does not compile

        long count = names.stream().filter(n -> n.length() > 2).count();
        System.out.println(count);      // 2
    }
}

Does it cost anything?

A fair question, since forEach creates a lambda object where a loop creates nothing. In practice the answer is no, for two reasons worth knowing rather than taking on faith.

First, a lambda that captures nothing — System.out::println, or n -> n.trim() — is instantiated once and reused, not allocated per call. Only a capturing lambda creates an object, and even then it is one object for the whole loop, not one per element.

Second, the JIT compiler inlines both forms into much the same machine code once the method is hot. For a collection of any realistic size the difference is unmeasurable, and for a collection small enough that it might matter, the whole operation is already free.

So choose on readability. The one real performance caveat is the opposite of the usual worry: parallelStream().forEach(...) is genuinely slower than a loop for small collections, because coordinating threads costs more than the work being coordinated.

Choosing between them

UseWhen
forEachthe body is one short action, especially a method reference
forEach on a mapyou want key and value without unpacking an Entry
for-each loopyou need break, the index, or a body longer than a couple of lines
neitheryou are building a result — use map/collect, not a side effect

That last row is the one worth internalising. Code like stream().forEach(list::add) is a pipeline pretending to be a loop; it breaks under parallelism and reads worse than stream().toList(). If a forEach is filling something in, it is the wrong tool.

Next

Default and static interface methods is next — how forEach was added to Iterable without breaking every class that already implemented it.