Streams
A stream is a pipeline that pulls elements from a source through a chain of operations to a single terminal result. Nothing runs until the terminal operation asks for elements, and each element travels through the whole chain before the next one starts. This page explains that model, builds pipelines from different sources, shows laziness and its consequences, warns about parallel streams, and measures streams against loops.
Contents
The stream model
The Stream API, added in Java 8 in java.util.stream
- A source: a collection's
, an array,stream() ,IntStream.range , a file's lines, or a generator.Stream.of - Zero or more intermediate operations such as
,filter ,map andsorted . Each returns a new stream and does no work yet.distinct - One terminal operation such as
,toList ,count ,sum orforEach , which runs the pipeline and produces a result.collect
Execution is lazy and element by element: the terminal operation pulls one element from the source, passes it through every stage, and only then pulls the next. Short-circuiting operations such as findFirstanyMatchlimitsorted
| Feature | Since | Use |
|---|---|---|
StreamIntStreamLongStreamDoubleStreamCollectors | Java 8 | The core API; primitive streams avoid boxing |
takeWhiledropWhileiterate(seed, hasNext, next)ofNullable | Java 9 | Prefixes of ordered streams, bounded generation |
Collectors.teeing | Java 12 | Two results in one pass |
Stream.toList()mapMulti | Java 16 | Short unmodifiable results, flexible one-to-many |
Gatherers (Stream.gather | Java 24 (JEP 485; previewed in 22 and 23) | Custom intermediate operations (not used here) |
Every example was run on JDK 21. Grouping and flattening have their own pages:
A pipeline from start to finish
A short list of orders, some paid and some not, is filtered, transformed and summed, the shape of most stream code in business applications.
import java.util.List;
public class Pipeline {
record Order(String customer, double amount, boolean paid) {
}
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("ann", 120.0, true), new Order("bob", 35.5, false),
new Order("cy", 410.0, true), new Order("ann", 60.0, true),
new Order("dee", 15.0, true));
// source -> intermediate operations -> one terminal operation
List<String> bigPayers = orders.stream() // source
.filter(Order::paid) // keep paid orders
.filter(o -> o.amount() >= 50) // keep amounts of 50 or more
.map(Order::customer) // Order -> String
.distinct() // drop repeated names
.sorted() // alphabetical
.toList(); // terminal: run it, collect a List
System.out.println("paid >= 50: " + bigPayers);
double paidTotal = orders.stream()
.filter(Order::paid)
.mapToDouble(Order::amount) // a DoubleStream, no boxing
.sum();
System.out.println("paid total: " + paidTotal);
long unpaid = orders.stream().filter(o -> !o.paid()).count();
System.out.println("unpaid: " + unpaid);
}
}
Output:
paid >= 50: [ann, cy]
paid total: 605.0
unpaid: 1
- Two
stages keep paid orders of 50 or more,filter turns eachmap into its customer name, andOrder runs the pipeline.toList() switches to amapToDouble , whoseDoubleStream adds primitivesum() s without creatingdouble objects.Double on the unpaid orders is another terminal operation; each total needed its own pipeline, because a stream cannot be reused.count()
Laziness and single use
The program prints inside the pipeline so that the order of work becomes visible, and then tries to reuse a stream.
import java.util.List;
import java.util.stream.Stream;
public class Laziness {
public static void main(String[] args) {
List<String> names = List.of("ada", "bo", "cleo", "dmitri", "eve");
// Nothing runs until the terminal operation
Stream<String> pipeline = names.stream()
.peek(n -> System.out.println(" looking at " + n))
.filter(n -> n.length() > 3)
.map(String::toUpperCase);
System.out.println("pipeline built, nothing printed yet");
// findFirst stops as soon as it has an answer: "dmitri" and "eve" are never looked at
System.out.println("first long name: " + pipeline.findFirst().orElse("none"));
// A stream can be consumed only once
try {
pipeline.count();
} catch (IllegalStateException e) {
System.out.println("reuse: " + e.getMessage());
}
}
}
Output:
pipeline built, nothing printed yet
looking at ada
looking at bo
looking at cleo
first long name: CLEO
reuse: stream has already been operated upon or closed
- Building the pipeline printed nothing; the work started only when
was called.findFirst - The stream looked at
,ada andbo and stopped as soon as it found a long name. The remaining names were never examined.cleo - Calling a second terminal operation on the same stream threw
: "stream has already been operated upon or closed". Store the source or aIllegalStateException , not the stream.Supplier<Stream> - Laziness means side effects in lambdas, such as logging or counting, happen only for the elements actually processed. Keep lambdas free of side effects where possible.
Sources, generators and reductions
Each line uses a different source or terminal operation, including the bounded generators and prefix operations added in Java 9.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class SourcesAndReduce {
public static void main(String[] args) {
// Different sources
System.out.println("range: " + IntStream.rangeClosed(1, 5).boxed().toList());
String joined = Stream.of("x", "y", "z").collect(Collectors.joining("-"));
System.out.println("of: " + joined);
System.out.println("array: " + Arrays.stream(new int[] {4, 8, 15}).sum());
System.out.println("iterate: " + Stream.iterate(1, n -> n * 2).limit(8).toList());
System.out.println("iterate with condition: "
+ Stream.iterate(1, n -> n < 100, n -> n * 3).toList());
System.out.println("chars: " + "hello".chars().filter(c -> c == 'l').count() + " l's");
// takeWhile / dropWhile work on order, unlike filter
List<Integer> temps = List.of(12, 14, 19, 23, 18, 25);
System.out.println("takeWhile < 20: " + temps.stream().takeWhile(t -> t < 20).toList());
System.out.println("filter < 20: " + temps.stream().filter(t -> t < 20).toList());
// reduce combines all elements into one
int product = IntStream.rangeClosed(1, 5).reduce(1, (a, b) -> a * b);
System.out.println("5! via reduce: " + product);
// Summary statistics in one pass
var stats = temps.stream().mapToInt(Integer::intValue).summaryStatistics();
System.out.println("stats: " + stats);
}
}
Output:
range: [1, 2, 3, 4, 5]
of: x-y-z
array: 27
iterate: [1, 2, 4, 8, 16, 32, 64, 128]
iterate with condition: [1, 3, 9, 27, 81]
chars: 2 l's
takeWhile < 20: [12, 14, 19]
filter < 20: [12, 14, 19, 18]
5! via reduce: 120
stats: IntSummaryStatistics{count=6, sum=111, min=12, average=18.500000, max=25}
,IntStream.rangeClosed ,Stream.of andArrays.stream are common sources.String.chars is infinite and needsStream.iterate(seed, next) ; the Java 9 formlimit stops by itself, like aiterate(seed, hasNext, next) loop.for stops at the first element that fails, so it returned 12, 14 and 19 but not the later 18, whichtakeWhile(x < 20) kept.filter is about position,takeWhile about every element.filter folds the elements into one value, here 5! = 120. The identity value must be neutral for the operation.reduce(1, (a, b) -> a * b) returns count, sum, minimum, average and maximum from one pass.summaryStatistics()
Parallel streams: correctness first
parallel()ForkJoinPool
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
public class ParallelCaution {
public static void main(String[] args) {
// Wrong: a shared ArrayList modified from many threads, tried 10 times
StringBuilder results = new StringBuilder();
for (int run = 0; run < 10; run++) {
List<Integer> unsafe = new ArrayList<>();
try {
IntStream.range(0, 100_000).parallel().forEach(unsafe::add);
results.append(unsafe.size()).append(' ');
} catch (ArrayIndexOutOfBoundsException e) {
results.append("AIOOBE ");
}
}
System.out.println("shared ArrayList, 10 runs: " + results.toString().trim());
// Right: let the stream build the result
List<Integer> safe = IntStream.range(0, 100_000).parallel().boxed().toList();
System.out.println("collected size: " + safe.size());
// forEach on a parallel stream does not keep order; forEachOrdered does
List<Integer> seen = Collections.synchronizedList(new ArrayList<>());
IntStream.range(0, 10).parallel().forEach(seen::add);
String order = isSorted(seen) ? "sorted" : "not sorted";
System.out.println("parallel forEach order: " + order + " this run");
}
static boolean isSorted(List<Integer> list) {
for (int i = 1; i < list.size(); i++) {
if (list.get(i - 1) > list.get(i)) {
return false;
}
}
return true;
}
}
Output (results of the first and last lines vary between runs):
shared ArrayList, 10 runs: 94279 96140 100000 50000 75000 100000 100000 100000 100000 25000
collected size: 100000
parallel forEach order: not sorted this run
- Adding to a shared
from a parallelArrayList is a data race. The program tried it 10 times: some runs lost elements, some happened to keep all 100,000, and a run can also throwforEach , shown asArrayIndexOutOfBoundsException . Code that works once proves nothing;AIOOBE is not thread-safe.ArrayList - Letting the stream build the result with
or a collector is safe: each thread fills its own part and the parts are merged in order.toList() on a parallel stream runs in whatever order the threads finish;forEach restores encounter order at a cost.forEachOrdered
Streams versus loops, sequential versus parallel
The program runs a cheap computation, summing squares, and an expensive one, testing primes by trial division, as a loop, a primitive stream, a parallel stream and a boxed stream. For each version the program keeps the quickest of three runs.
import java.util.function.LongSupplier;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
public class StreamSpeed {
public static void main(String[] args) {
System.out.println("cheap work: sum of squares of 0..9,999,999");
time("for loop", () -> {
long s = 0;
for (long i = 0; i < 10_000_000; i++) {
s += i * i;
}
return s;
});
time("LongStream", () -> LongStream.range(0, 10_000_000).map(i -> i * i).sum());
time("LongStream.parallel()", () -> LongStream.range(0, 10_000_000).parallel()
.map(i -> i * i).sum());
time("Stream<Long> (boxed)", () -> LongStream.range(0, 10_000_000).boxed()
.map(i -> i * i).reduce(0L, Long::sum));
System.out.println("expensive work: count primes below 300,000");
time("for loop", () -> {
long c = 0;
for (int i = 0; i < 300_000; i++) {
if (isPrime(i)) {
c++;
}
}
return c;
});
time("IntStream", () -> IntStream.range(0, 300_000).filter(StreamSpeed::isPrime).count());
time("IntStream.parallel()", () -> IntStream.range(0, 300_000).parallel()
.filter(StreamSpeed::isPrime).count());
System.out.println("available processors: " + Runtime.getRuntime().availableProcessors());
}
static boolean isPrime(int n) {
if (n < 2) {
return false;
}
for (int d = 2; (long) d * d <= n; d++) {
if (n % d == 0) {
return false;
}
}
return true;
}
// Best of 3 runs
static void time(String name, LongSupplier f) {
long best = Long.MAX_VALUE;
long result = 0;
for (int run = 0; run < 3; run++) {
long t0 = System.nanoTime();
result = f.getAsLong();
best = Math.min(best, System.nanoTime() - t0);
}
System.out.printf(" %-24s %7.1f ms (%d)%n", name, best / 1e6, result);
}
}
Output (2-CPU Linux machine, JDK 21; rough timings):
cheap work: sum of squares of 0..9,999,999
for loop 3.6 ms (1291890006563070912)
LongStream 3.8 ms (1291890006563070912)
LongStream.parallel() 2.1 ms (1291890006563070912)
Stream<Long> (boxed) 58.2 ms (1291890006563070912)
expensive work: count primes below 300,000
for loop 20.3 ms (25997)
IntStream 20.4 ms (25997)
IntStream.parallel() 12.8 ms (25997)
available processors: 2
- A primitive
was as fast as the loop: after JIT compilation, a simple stream pipeline becomes a similar loop.LongStream - The boxed
was more than ten times slower, because every value became aStream<Long> object. UseLong ,mapToInt and the primitive streams for numbers.mapToLong - With two processors, the parallel versions gained between nothing and about 1.7 times across runs, short of the ideal factor of two; splitting, scheduling and merging cost something, and other work on the machine competes for the same cores. With tiny per-element work and small inputs, parallel streams are often slower than sequential ones.
- Parallelism pays when the work per element is significant, the source splits well (arrays,
, ranges), and nothing shared is modified.ArrayList
Stateful operations: sorted, distinct and limit
Most intermediate operations are stateless: filtermap
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class StatefulOps {
public static void main(String[] args) {
// 1. Stateless stages pass each element straight on; sorted must see all of them first
List<String> log = new ArrayList<>();
Stream.of(3, 1, 2)
.peek(n -> log.add("in " + n))
.map(n -> n * 10)
.peek(n -> log.add("mapped " + n))
.forEach(n -> log.add("out " + n));
System.out.println("without sorted: " + log);
log.clear();
Stream.of(3, 1, 2)
.peek(n -> log.add("in " + n))
.sorted()
.forEach(n -> log.add("out " + n));
System.out.println("with sorted: " + log);
// 2. sorted before findFirst still reads everything; min reads everything once, no sort
int[] seen = {0};
int smallest = IntStream.range(0, 1_000_000).map(i -> 999_999 - i)
.peek(i -> seen[0]++).sorted().findFirst().orElseThrow();
System.out.println("sorted().findFirst() = " + smallest + " after reading " + seen[0]);
seen[0] = 0;
int viaMin = IntStream.range(0, 1_000_000).map(i -> 999_999 - i)
.peek(i -> seen[0]++).min().orElseThrow();
System.out.println("min() = " + viaMin + " after reading " + seen[0] + ", without sorting");
// 3. On an infinite stream, a stateful stage before limit never finishes; after it is fine
List<Integer> firstEvens = Stream.iterate(1, n -> n + 1)
.filter(n -> n % 2 == 0).limit(5).sorted().toList();
System.out.println("limit then sorted on an infinite stream: " + firstEvens);
// 4. distinct keeps the first occurrence in encounter order
System.out.println("distinct: " + Stream.of("b", "a", "b", "c", "a").distinct().toList());
// 5. limit on an ordered parallel stream must keep the first n; unordered() relaxes it
List<Integer> ordered = IntStream.range(0, 1_000).boxed().parallel().limit(5).toList();
List<Integer> any = IntStream.range(0, 1_000).boxed().parallel().unordered().limit(5)
.toList();
System.out.println("parallel limit(5): " + ordered + ", after unordered(): " + any.size()
+ " elements, not necessarily the first five");
}
}
without sorted: [in 3, mapped 30, out 30, in 1, mapped 10, out 10, in 2, mapped 20, out 20]
with sorted: [in 3, in 1, in 2, out 1, out 2, out 3]
sorted().findFirst() = 0 after reading 1000000
min() = 0 after reading 1000000, without sorting
limit then sorted on an infinite stream: [2, 4, 6, 8, 10]
distinct: [b, a, c]
parallel limit(5): [0, 1, 2, 3, 4], after unordered(): 5 elements, not necessarily the first five
- Without
, each element went all the way through before the next started:sorted , then 1, then 2. Within 3, mapped 30, out 30 , all three came in before anything came out, because the smallest element might be the last one read.sorted read all 1,000,000 elements and sorted them to return 0.sorted().findFirst() normally stops early, but not after a stage that needs every element.findFirst also read all of them, which a minimum must do, but skipped the O(n log n) sort.min()- On an infinite stream,
placed beforelimit(5) producedsorted . With[2, 4, 6, 8, 10] first, the program would never finish, sincesorted would wait for the end of a stream that has none.sorted kept the first occurrence of each value in encounter order,distinct . It remembers every value it has seen in a set, so on a large stream with few repeats it holds nearly the whole stream in memory.[b, a, c]- A parallel
on an ordered source still returnedlimit(5) , which forces the threads to coordinate. After[0, 1, 2, 3, 4] it may return any five elements, and the documentation notes that relaxing the order can makeunordered() much cheaper in parallel.limit
reduce versus collect
Both terminal operations combine the elements into one result, but they suit different results. reducecollectStringBuilder
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ReduceVsCollect {
public static void main(String[] args) {
List<String> words = List.of("stream", "map", "filter", "reduce", "collect");
// 1. reduce with an identity and an associative operator works in parallel too
int letters = words.parallelStream().map(String::length).reduce(0, Integer::sum);
System.out.println("total letters (reduce): " + letters);
// 2. Three-argument reduce: accumulate into a different type, combine partial results
int viaThreeArg = words.parallelStream()
.reduce(0, (sum, w) -> sum + w.length(), Integer::sum);
System.out.println("total letters (3-arg reduce): " + viaThreeArg);
// 3. A non-associative operator gives different answers sequentially and in parallel
int seq = IntStream.rangeClosed(1, 8).reduce(0, (a, b) -> a - b);
int par = IntStream.rangeClosed(1, 8).parallel().reduce(0, (a, b) -> a - b);
System.out.println("subtraction: sequential " + seq + ", parallel " + par);
// 4. A wrong identity is counted once per parallel chunk
int seqPlus10 = IntStream.rangeClosed(1, 8).reduce(10, Integer::sum);
int parPlus10 = IntStream.rangeClosed(1, 8).parallel().reduce(10, Integer::sum);
System.out.println("identity 10: sequential " + seqPlus10 + ", parallel " + parPlus10);
// 5. reduce is for values; mutable containers belong in collect
int bad = 0;
for (int run = 0; run < 20; run++) {
List<Integer> shared = new ArrayList<>(); // one identity shared by every thread
try {
List<Integer> r = IntStream.range(0, 1_000).boxed().parallel()
.reduce(shared, (list, n) -> {
list.add(n);
return list;
}, (x, y) -> {
if (x != y) {
x.addAll(y);
}
return x;
});
if (!r.equals(IntStream.range(0, 1_000).boxed().toList())) {
bad++;
}
} catch (RuntimeException e) {
bad++;
}
}
System.out.println("reduce into one shared ArrayList, parallel: wrong or crashed in "
+ bad + " of 20 runs");
ArrayList<Integer> collected = IntStream.range(0, 1_000).boxed().parallel()
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println("collect(ArrayList::new, add, addAll), parallel: size "
+ collected.size() + ", first " + collected.subList(0, 3));
// 6. String building: joining is a collect, not a reduce of strings
String joined = words.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println("joining: " + joined);
String slow = Stream.of("a", "b", "c").reduce("", String::concat);
System.out.println("reduce(\"\", concat): " + slow + " (copies the string at every step)");
}
}
total letters (reduce): 28
total letters (3-arg reduce): 28
subtraction: sequential -36, parallel 0
identity 10: sequential 46, parallel 76
reduce into one shared ArrayList, parallel: wrong or crashed in 20 of 20 runs
collect(ArrayList::new, add, addAll), parallel: size 1000, first [0, 1, 2]
joining: [stream, map, filter, reduce, collect]
reduce("", concat): abc (copies the string at every step)
- Summing word lengths with
gave 28 in parallel, the same as sequentially. Addition is associative and 0 is a true identity, the two conditions thereduce(0, Integer::sum) documentation requires.reduce - The three-argument form folds each word into an
running total and usesint to merge the totals of different threads. It avoids a separateInteger::sum step, at the cost of a less readable call.map - Subtraction is not associative, so the same reduction gave -36 sequentially and 0 in parallel. The parallel stream split the numbers, reduced each part and then subtracted one part's result from another's.
- With 10 as the "identity" for addition, the sequential sum was 46, but the parallel sum was 76: the 10 was added once for every chunk. An identity must leave every value unchanged.
- Reducing into one shared
was wrong or crashed in all 20 parallel runs. All threads added to the same list at once, which breaks it, and even without a crash the order was scrambled.ArrayList gave each thread a fresh list and merged them in order, ending with all 1,000 elements startingcollect(ArrayList::new, ArrayList::add, ArrayList::addAll) .[0, 1, 2] appends to oneCollectors.joining .StringBuilder gave the samereduce("", String::concat) but creates a new string for every element, which costs time in proportion to the square of the length on long streams.abc
When to use streams
| Situation | Prefer |
|---|---|
| Filter, transform and collect a collection | A stream |
| Numeric totals and statistics | A primitive stream: mapToInt(...).sum() |
| Find the first match | filter(...).findFirst()break |
| Logic with several exits, checked exceptions or index arithmetic | A loop |
| Modify the source collection | removeIfreplaceAll |
| Heavy work per element, many cores | Try parallel() |
| Results from a parallel stream | A collector, never a shared mutable list |
| The same data processed twice | Two streams from the source |
For filtering and searching lists specifically, see