When a stream is the wrong tool
Boxing, megamorphic call sites, and the loop that is genuinely clearer.
Open this lesson in the learning hubKey points
Stream<Integer>boxes every element. Over millions of values that allocation dominates, andIntStreamexists 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
findFirstor 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.