Streams in Java
Streams a new addition to Java 8 enables you to perform operations on a sequential data in a pipelined fashion and return a result.
Streams are useful when it is necessary to perform the same set of operations on multiple values of the same data type. Say, you have a sequence of string values and you are filtering out values greater than length 5 your code will look something like this:
This code could be still simplified to
Here, the list inputValues is converted to a stream by calling the method stream() it. The filter() method takes a condition that needs to be evaluated on each of the values. Here, we pass a lambda expression to filter out values that are greater than length 5. Finally, the values that are returned from the previous operation are converted and collected as a List by using the Collectors.toList().
Using streams not reduces the length of code, but also improves the readability. This is a simple example to show how useful streams can be.
Stream Pipeline:
Stream operations are performed in a pipeline fashion with the output of each operation being used as input to the next operation. This pipeline consists of 3 main parts:
- Source of stream → mandatory part
- Intermediate operations → optional part
- Terminal operation → mandatory part
Stream Source:
This generates the data to work on. Stream sources can be finite or infinite. The source is mandatory as a part of the pipeline.
Intermediate Operation:
These methods operate on a stream of values and return a stream of values. A stream pipeline can have zero or more intermediate operations. Some of the intermediate operations available in stream are:
filter()
Returns the stream of values that match the filter condition.
Stream<T> filter(Predicate<? super T> predicate)
distinct()
Returns the distinct values in the sequence.
Stream<T> distinct()
map()
Returns a 1–1 mapped value of elements in the stream based on the mapping function passed.
<R> Stream<R> map(Function<? super T, ? super R>) mappingFunc)
flatMap()
It takes in parameter a list of stream and returns a single stream of values combining the values from all the streams. This method also removes empty values if present in the stream.
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
Terminal Operation:
If you have noticed in the above section, all the intermediate operations were followed by some function. This function is called a terminal operator and is an important part of the stream pipeline. None of the intermediate operations, if present, would be invoked unless a terminal method is present.
If you have noticed from the above examples, you would observe forEach() and collect() fall into the terminal operations available in streams. The return value of a terminal operator could be any type, based on the function used, unlike the intermediate operations which always returns a stream.
Some of the terminal operators in streams are:
count()
Returns the count of elements in the stream. This method does not terminate for an infinite stream.
long count()
findAny() and findFirst()
findAny() and findFirst() returns an element from the stream unless the stream is empty. In case the stream is empty, an optional is returned. This method terminates for an infinite stream.
Optional<T> findAny()
Optional<T> findFirst()
allMatch(), anyMatch(), noneMatch()
All of them return a boolean if the condition satisfies. They may not terminate if the stream is infinite.
boolean allMatch(Predicate<? super T> pred)
boolean anyMatch(Predicate<? super T> pred)
boolean noneMatch(Predicate<? super T> pred)
Another interesting point about terminal operator is that, it signifies that stream is closed for further operation. Meaning, the same stream cannot be used to invoke other terminal or intermediate operations again. It will throw an error saying “stream has already been operated upon or closed”