We have been discussing the Java 8 Streams API since couple of our posts. We have had an overview of the Java 8 Streams API, the Laziness and the Performance improvements it brings, and the Intermediate Operations it provides. Today, we are going to cover various Terminal Operations provided by the streams api.
Usual stream operation flow can have a pipe of multiple intermediate operations and a terminal operation at the end. The intermediate operations are called upon streams and their return type is stream. Hence, they can be easily chained together in order to get a complete processing pipeline. Any of such a stream pipeline must end with a valid Terminal Operation. Let’s discuss more about the terminal operations in detail.
Conditional Matching and Finding:
While working on collections, it is usual requirement to find one or more elements matching to a condition. We have seen similar operations in the last post, but they were intermediate operations. Remember intermediate operations return stream as a response, while the terminal operations return non stream objects and they stand at the end of a processing pipeline. Let’s have a look at the Java 8 Steams API terminal operations which are used for finding or matching elements in a stream.
The Java 8 Streams API provides number of useful matching functions that can be used for matching a provided predicate against each element of a stream.
At least one element matches to the predicate:
When we want to check if at least one element is present in the given stream that matches to the given predicate, we can use anyMatch function. This function returns a boolean value.
//Check if at least one student has got distinction Boolean hasStudentWithDistinction = students.stream() .anyMatch(student - > student.getScore() > 80);
All elements match to the predicate:
Similar to the anyMatch when we want to check if all of the elements in a stream match with the provided predicate. This method also returns a boolean.
//Check if All of the students have distinction Boolean hasAllStudentsWithDistinction = students.stream() .allMatch(student - > student.getScore() > 80);
None of the elements matches to the predicate:
The noneMatch function returns true if none of the elements in a given stream matches with the given predicate.
//Return true if None of the students are over distinction Boolean hasAllStudentsBelowDistinction = students.stream() .noneMatch(student - > student.getScore() > 80);
Java 8 Streams API provides two methods for the finding purpose- findAny and findFirst. The findAny method returns any element from a given stream, while the findFirst returns the first element from the given stream.
On high level this methods do not sound useful enough, but they are. If we have an intermediate filtering operation that verifies all of the elements in a stream against some predicate, the findAny and findFirst can be used immediately after that to get any element matching to the filter or first element matching to the filter. The usefulness of these method is more visible when we work in parallel environments where a stream is processed in parallelism. It’s very difficult to find the first element or any random element otherwise.
//Returns any student that matches to the given condition students.stream().filter(student - > student.getAge() > 20) .findAny(); //Returns first student that matches to the given condition students.stream().filter(student - > student.getAge() > 20) .findFirst();
The reducing has a capability of processing the elements in a stream repeatedly to produce an output in the form of a single element. Reducing reduces the entire stream into a single value. The reduce operation is very useful for calculating the sum of all elements in the stream or calculating max or min element out of a stream.
In functional languages there is a concept of fold. The reduce is quite similar to the fold operation. The reduce function accepts an identity or a starting value of the output, and then it combines the identity with the first element of the stream, the result is then combined with the second element of the stream and so on. The logic, how the elements are combined together, is provided as an accumulator.
Summing and Multiplying:
We all know, summing all of the elements in a collection needs the resulting variable set to an initial value (zero), and then combining the result with each element of the collection (result += element).
The reduce function simplifies this with the help of internal iterations. See the below examples where summing is done by reduce operation.
//Summing all elements of a stream Integer sum = numbers.stream() .reduce(0, (x, y) - > x + y); //reduce(identity, accumulator) // Similarly below is an example of finding the product of all of the elements in a stream. Integer product = numbers.stream() .reduce(1, (x, y) - > x * y);
There is an overloaded version of the reduce method, which does not take the identity. In that case the resulting value will be wrapped under Optional. Before the end of the post we will try to understand a bit of Optional.
//Summing without passing an identity Optional < integer > sum = numbers.stream() .reduce((x, y) - > x + y); //Product without passing an identity Optional < integer > product = numbers.stream() .reduce((x, y) - > x * y);
Min and Max:
Finding min and max is also made very easy with the help of reduce operation. See the below example.
//Min of the stream Optional < integer > min = numbers.stream() .reduce(0, Integer::min); //Max of the stream Optional < integer > max = numbers.stream() .reduce(0, Integer::max);
Integer and the other numerical wrappers have been provided with static min and max methods both of these methods takes two elements (first and the next). The reduce operation calls these methods repeatedly and passes the elements in the stream one by one.
The reduce operation is extremely useful when the stream is being processed under parallel fashion. In parallel processing it is difficult to maintain and share the state of the variable, that hold the incremental summation, across the various processing units. Most of the times the considerable amount performance gain, achieved with the parallel processing, is sacrificed for making the shared variables thread safe.
The Java 8 Streams reduce method, when called upon parallel streams, internally hides this jargon, and we simply do not need to be worried about. In such cases the reduce makes use of Map Reduce to perform the calculations. The stream is partitioned into pieces and all of the pieces are processed in parallel. Each processing unit will have it’s own resulting variable, and hence there is no concern of shared state. All of these results are then combined together to get the final result.
One thing which may go wrong the the matching and finding elements is that there might be a case when no element is returned by them. In such a case these methods simply return Null. This may be error-prone to the client codes and the client program needs to put a Null check. Java 8 comes up with a special class which helps solving this problem. The Optional class represents whether an object is assigned or unassigned (Null).
The methods like findAny, findFirst, and reduce (when called without providing an identity) return values wrapped under Optional. We can call all of the stream like operations on Optional as well. If that sounds tricky, let it be. May be someday we will discuss more on the Optional thing. Right now, we will look at the use of Optional in matching and finding operations.
students.stream() .filter(student - > student.getScore() > 80) // filter .findAny() //Any student matching to the filter .map(Student::getName) // mapping to students name .ifPresent(System.out::println); // print if name of the student is not Null
In the example above, we can see the use of findAny just after the filter. The findAny returns Optional instance. The map method here, though it looks like a stream operation, is called on Optional instance returned by findAny. The Optional class’s map method returns Optional. Now, the ifPresent simply performs the Null checking and executes the function passed to it, if the Optional instance contains a value inside it.
So, we had an overview of various Terminal Operations provided by Java 8 Streams API. But we are still left with the last, but probably the most important terminal operation, and that is collect. The collect operation surely deserve a detailed discussion, and hence is left here. Soon, we will meet again with our next article and that will be on Collecting the elements out of streams.
Thanks for visiting. I hope you found the tutorials and code examples helpful. Please post your comments, questions, and suggestions below.
You can also reach out to me on any of the Social platforms listed here.
Happy Coding !!