A Stream
will only be traversed when there is a terminal operation, like count()
, collect()
or forEach()
. Otherwise, no operation on the Stream
will be performed.
In the following example, no terminal operation is added to the Stream
, so the filter()
operation will not be invoked and no output will be produced because peek()
is NOT a terminal operation.
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
This is a Stream
sequence with a valid terminal operation, thus an output is produced.
You could also use forEach
instead of peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Output:
2
4
6
8
After the terminal operation is performed, the Stream
is consumed and cannot be reused.
Although a given stream object cannot be reused, it's easy to create a reusable Iterable
that delegates to a stream pipeline. This can be useful for returning a modified view of a live data set without having to collect results into a temporary structure.
List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();
for (String str : iterable) {
System.out.println(str);
}
for (String str : iterable) {
System.out.println(str);
}
Output:
foo
bar
foo
bar
This works because Iterable
declares a single abstract method Iterator<T> iterator()
. That makes it effectively a functional interface, implemented by a lambda that creates a new stream on each call.
In general, a Stream
operates as shown in the following image:
NOTE: Argument checks are always performed, even without a terminal operation:
try {
IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}
Output:
We got a NullPointerException as null was passed as an argument to filter()