With this post, we start discussing terminal operations–the last from the three Stream methods categories: factory methods, intermediate operations, and terminal operations.
Terminal operations are the most important operations of the Stream interface. A terminal operation produces side effects or/and a single value. The operation that produces side effects only can be used for a finite and infinite Stream. But a terminal operation that produces a single value never returns any value for an infinite Stream.
No other operation can be applied downstream of a terminal operation.
We have used already the forEach(Consumer<T>) terminal operation to print each element. It does not return a value, thus it is used for its side effects.
The Stream interface has many more powerful terminal operations. It is possible to accomplish everything in them without using any other operations. Chief among the terminal operations is the collect() operation, which has two forms:
– R collect(Collector<T, A, R> collector), and
– R collect(Supplier<R> supplier, BiConsumer<R, T> accumulator, BiConsumer<R, R> combiner).
It allows composing practically any process that can be applied to a stream. The classic example is as follows:
List<String> list = Stream.of("1", "2", "3", "4", "5")
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
System.out.println(list); //prints: [1, 2, 3, 4, 5]
In this example, the collect() operation is used in such a way as to be suitable for parallel processing too. The first parameter of the collect() operation is a function that produces a value based on the stream element. The second parameter is the function that accumulates the result. And the third parameter is the function that combines the accumulated results from all the threads that processed the stream.
In addition to that, the API authors added to the interface Stream various even more specialized terminal operations that are much simpler and easier to use. In this post, we will review the first three terminal operations. Only after we discuss all specialized operations, we will look at the plethora of Collector objects produced by the Collectors class.
So, let’s start with three simple terminal operations:
– long count(). Counts all elements in this stream.
– void forEach(Consumer<T> action). Applies the provided action to each element of this stream.
– void forEachOrdered(Consumer<T> action). Applies the provided action to each element of this stream in the order the elements are emitted by the source Observable, even if the stream is parallel.
long count()
The count() terminal operation of the Stream interface looks straightforward and benign. It returns the number of elements in this stream. Those who are used to working with collections and arrays may use the count() operation without thinking twice. But the following code demonstrates a potential pitfall:
long count = Stream.of("1", "2", "3", "4", "5")
.peek(System.out::print) //prints nothing
.count();
System.out.print(count); //prints: 5
Have you got into this trap too, inspecting the peek() operation printing every element?
The catch is that the count() method is able to determine the stream size without executing all the pipeline. That’s why the peek() operation did not print anything.
Another possible pitfall is that it is not always possible to determine the stream size at the source. For example, the stream may be infinite. So, use count() with care.
Since we are on the topic, another possible way to determine the stream size is by using the collect() operation:
long count = Stream.of("1", "2", "3", "4", "5")
.peek(System.out::print) //prints: 12345
.collect(Collectors.counting());
System.out.println(count); //prints: 5
As you can see, the collect() operation does not calculate the stream size at the source. It just applies the passed-in collector to the stream. And the collector counts the elements provided to it by the collect() operation. It helps in some cases, but still does not finish the job in the case of an infinite stream. So, beware.
void forEach(Consumer
The forEach(Consumer<T> action) operation just applies the provided action to each the value that it receives:
Stream.of("1","2","3","4","5")
.forEach(System.out::print); //prints: 12345
Just beware that in the case of a parallel stream, the order of the processing of the elements becomes unpredictable:
Stream.of("1","2","3","4","5")
.parallel()
.forEach(System.out::print); //prints: 34215
If the order of processing of the elements of a parallel stream is important, use the forEachOrdered(Consumer<T> action) operator.
void forEachOrdered(Consumer
There is no difference between forEach() and forEachOrdered() in the result if the stream is sequential. But in the case of a parallel stream processing, there is a big difference. The following is the sequential stream processing:
Stream.of("1","2","3","4","5")
.forEachOrdered(System.out::print); //prints: 12345
And here how the result of a parallel stream processing looks like:
Stream.of("1","2","3","4","5")
.parallel()
.forEachOrdered(System.out::print); //prints: 12345
In the case of forEachOrdered(), the processing order is defined at the source and does not depend on the executing system. So, use the forEachOrdered() operator for a parallel stream processing, but only if the order in which you need the elements to be processed is important and it has to be the order the values are arranged at the source.
In the next post, we will continue discussing terminal operations and review the following three:
– boolean allMatch(Predicate<T> predicate)
– boolean anyMatch(Predicate<T> predicate)
– boolean noneMatch(Predicate<T> predicate)
See other posts on Java 8 streams and posts on other topics.
You can also use navigation pages for Java stream related blogs:
— Java 8 streams blog titles
— Create stream
— Stream operations
— Stream operation collect()
The source code of all the code examples is here in GitHub.
Send your comments using the link Contact or in response to my newsletter.
If you do not receive the newsletter, subscribe via link Subscribe under Contact.