When a stream is the wrong tool

Streams · lesson 42 of 42 · 6 min read

Boxing, megamorphic call sites, and the loop that is genuinely clearer.

Open this lesson in the learning hub

Key points

  • Stream<Integer> boxes every element. Over millions of values that allocation dominates, and IntStream exists specifically to avoid it - the difference is often several times, not a few percent.
  • A short pipeline over a small collection is dominated by setup. For a handful of elements in a hot path, a plain loop is measurably faster and no less readable.
  • Streams are hard to escape early in a way a loop makes trivial. There is no way to break with a partial result and an index; you either use findFirst or accept that the shape does not fit.
  • Debugging is worse. A stack trace through a pipeline is full of framework frames, and stepping through lambdas in a debugger is far less pleasant than stepping through a loop.
  • Checked exceptions do not fit the functional interfaces, so any pipeline calling code that throws one needs a wrapper - which is friction the loop simply does not have.
  • The genuine wins are declarative grouping, flattening and parallel-friendly transformation. Use them there, and stop pretending a three-element loop is improved by becoming a pipeline.

Example

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

public class WhenStreamsCost {

    static long time(String label, Runnable r) {
        r.run();                                   // warm up
        long start = System.nanoTime();
        for (int i = 0; i < 20; i++) { r.run(); }
        long ms = (System.nanoTime() - start) / 1_000_000;
        System.out.printf("  %-38s %5d ms%n", label, ms);
        return ms;
    }

    public static void main(String[] args) {
        int n = 2_000_000;
        int[] primitives = new int[n];
        List<Integer> boxed = new ArrayList<>(n);
        for (int i = 0; i < n; i++) { primitives[i] = i; boxed.add(i); }

        System.out.println("Summing " + n + " values, 20 times:");
        time("for loop over int[]", () -> {
            long sum = 0;
            for (int v : primitives) { sum += v; }
        });
        time("IntStream over int[]", () -> IntStream.of(primitives).asLongStream().sum());
        time("Stream<Integer> (boxed)", () ->
                boxed.stream().mapToLong(Integer::longValue).sum());
        time("Stream<Integer> reduce (worst)", () ->
                boxed.stream().reduce(0, Integer::sum));

        // Early exit with an index - natural in a loop, awkward in a stream.
        System.out.println();
        int target = 1_999_999;
        long start = System.nanoTime();
        int foundAt = -1;
        for (int i = 0; i < primitives.length; i++) {
            if (primitives[i] == target) { foundAt = i; break; }
        }
        System.out.println("loop found index " + foundAt + " in "
                + (System.nanoTime() - start) / 1000 + " us");

        // The stream version cannot give the index without extra work.
        start = System.nanoTime();
        int idx = IntStream.range(0, primitives.length)
                .filter(i -> primitives[i] == target)
                .findFirst().orElse(-1);
        System.out.println("stream found index " + idx + " in "
                + (System.nanoTime() - start) / 1000 + " us");

        System.out.println();
        System.out.println("Use streams for grouping, flattening and declarative");
        System.out.println("transformation. Use a loop for hot numeric paths and");
        System.out.println("anything needing an index or an early exit.");
    }
}

Boxing dominates numeric pipelines and streams cannot break with an index - reach for IntStream, or a loop.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Streams course, and every lesson in it is listed on the Streams contents page.