What is Stream?
A Stream represents a sequence of objects that supports various methods which can be pipelined to produce desired result. Stream does not store data. It operates on source data structures such as List, Collection, and Array etc.
Stream operations types
There are two types of stream operations.
1. Intermediate operations
Returns a stream that can be chained with other intermediate operations with dot.
2. Terminal operations
Returns void or non stream output.
Example
Output
Stream pipeline
To perform a sequence of operations over the elements of the data source and aggregate their results, we need three parts: the source, intermediate operation(s) and a terminal operation.
Intermediate operations return a new modified stream.
For example, in below example stream pipeline consists of,
source -> stringList
Intermediate operation -> map
Terminal operation -> forEach
Most stream operations accept parameters as that describes user-defined behavior, such as lambda expression map ((s) ->s.toUpperCase()) is passed to map operation.
To get correct behavior, streams parameters should be,
non-interfering: Stream source should not be modified while execution of stream pipeline.
Stateless: In most cases, lambda expressions should be stateless. Its output should not depend on state that might change during execution of Stream pipeline.
Stream Creation
There are multiple ways to create the Stream.
Empty Stream
empty () method can be used to create an empty stream. It is generally used to return Stream with zero elements rather than null.
Collection Stream
Stream can be created from Collection by calling .stream() or .parallelStream()
stringList.stream() will return you regular object stream.
Stream of()
You don’t need to create collection to get a Stream. You can also use .of()
Stream generate()
generate () method accepts Supplier for element generation. It creates infinite Stream and you can limit it by calling limit () function.
This will create Integer stream with 10 elements with value 1.
Stream iterate()
Stream.iterate() can also be used to generate infinite stream.
The first element of the resulting stream is the first parameter of the iterate() method. When creating every following element, the specified function is applied to the previous element. In the example above the second element will be 42.
Lazy Invocation
Stream intermediate operations are not executed until terminal operation is executed.
Each intermediate operation generates a new stream, stores the provided operation or function. When terminal operation is invoked, stream pipeline execution starts and all the intermediate operations are executed one by one.
Example
Output
In previous output, unless and until terminal operation count is called, nothing was printed on console. Here peek () method is used to print the element of stream.
Order of operations
In steams order of operations might be surprising.
A common approach will be to perform intermediate operation on all elements and then perform next operation.
In below example, strings tohfatul and sudha did not go through map and filter operation because we already got findAny result letter starts with ‘r’ that is rupali.
Example
Output
Primitive Streams
Java 8 offers the possibility to create streams out of three primitive types: int, long and double. As Stream<T> is a generic interface, and there is no way to use primitives as a type parameter with generics, three new special interfaces were created: IntStream for int, LongStream for long, DoubleStream for double.
All the primitive Streams are similar to regular Stream with following differences.
It supports few terminal aggregate functions such sum (), average (), etc.
It accepts specialized function interface such as IntPredicate instead of Predicate, IntConsumer instead of Consumer.
Example
The range(int startInclusive, int endExclusive) method creates an ordered stream from the first parameter to the second parameter. It increments the value of subsequent elements with the step equal to 1. The result doesn’t include the last parameter, it is just an upper bound of the sequence.
The rangeClosed(int startInclusive, int endInclusive) method does the same thing with only one difference, the second element is included. We can use these two methods to generate any of the three types of streams of primitives.
Since Java 8, the Random class provides a wide range of methods for generating streams of primitives. For example, the following code creates a DoubleStream, which has three elements:
Convert Stream to IntStream
You may need to convert Stream to IntStream to perform terminal aggregate operations such as sum or average. You can use mapToInt(), mapToLong() or mapToDouble() method to convert Stream to primitive Streams.
Example
Convert IntStream to Stream
You may need to convert IntStream to Stream to use it as any other datatype. You can use mapToObj() convert primitive Streams to regular Stream.
Example
Parallel streams
Java 8 introduced a way of accomplishing parallelism in a functional style using streams.
The API allows us to create parallel streams, which perform operations in a parallel mode. When the source of a stream is a Collection or an array, it can be achieved with the help of the parallelStream() method
Example:
If the source of a stream is something other than a Collection or an array, the parallel() method should be used:
Example:
When using streams in parallel mode, avoid blocking operations. It is also best to use parallel mode when tasks need a similar amount of time to execute. If one task lasts much longer than the other, it can slow down the complete app’s workflow
The stream in parallel mode can be converted back to the sequential mode by using the sequential() method:
Example:
Intermediate Operations
Let's understand the Stream intermediate operations with an example.
filter()
map()
flatMap()
distinct()
sorted()
peek()
limit()
skip()
filter()
The Java Stream filter() can be used to filter out elements from a Java Stream. The filter method takes a Predicate that is called for each element in the stream. If the element is to be included in the resulting Stream, the Predicate should return true. If the element should not be included, the Predicate should return false.
Example:
Output
Using filter() method to filter List of string objects:
map()
The Java Stream map() method converts (maps) an element to another object. For instance, if you had a list of strings it could convert each string to lowercase, uppercase, or to a substring of the original string, or something completely else.
Example:
Output
flatMap()
The Stream.flatMap() function, as the name suggests, is the combination of a map and a flat operation. This means you first apply the map function and then flatten the result.
To understand what flattening a stream consists in, consider a structure like [ [1,2,3],[4,5,6],[7,8,9] ] which has "two levels". It's basically a big List containing three more List. Flattening this means transforming it in a "one level" structure e.g. [ 1,2,3,4,5,6,7,8,9 ] i.e. just one list.
Example:
Output
distinct()
The Java Stream distinct() method is a non-terminal operation that returns a new Stream that will only contain the distinct elements from the original stream. Any duplicates will be eliminated.
Example:
Output
limit()
The Java Stream limit() method can limit the number of elements in a stream to a number given to the limit() method as a parameter. The limit() method returns a new Stream that will at most contain the given number of elements.
Example:
Output
peek()
The Java Stream peek() method is a non-terminal operation that takes a Consumer interface as a parameter. The Consumer will get called for each element in the stream. The peek() method returns a new Stream that contains all the elements in the original stream.
The purpose of the peek() method is, as the method says, to peek at the elements in the stream, not to transform them. Keep in mind that the peek method does not start the internal iteration of the elements in the stream. You need to call a terminal operation for that.
Example:
Output
As peek()‘s method: “This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline“.
Terminal Stream operations:
anyMatch()
allMatch()
noneMatch()
collect()
count()
findAny()
findFirst()
forEach()
min()
max()
reduce()
toArray()
anyMatch()
The Java Stream anyMatch() method is a terminal operation that takes a single Predicate as a parameter, starts the internal iteration of the Stream, and applies the Predicate parameter to each element.
Example:
Output
allMatch()
The Java Stream allMatch() method is a terminal operation that takes a single Predicate as the parameter, starts the internal iteration of elements in the Stream, and applies the Predicate parameter to each element.
If the Predicate returns true for all elements in the Stream, the allMatch() will return true. If not all elements match the Predicate, the allMatch() method returns false.
Example:
Output
noneMatch()
The Java Stream noneMatch() method is a terminal operation that will iterate the elements in the stream and return true or false, depending on whether no elements in the stream match the Predicate passed to noneMatch() as the parameter.
The noneMatch() method will return true if no elements are matched by the Predicate, and false if one or more elements are matched.
Example:
Output
collect()
The Java Stream collect() method is a terminal operation that starts the internal iteration of elements and collects the elements in the stream in a collection or object of some kind.
Example:
Output
count()
The Java Stream count() method is a terminal operation that starts the internal iteration of the elements in the Stream and counts the elements.
Example:
Output
findAny()
The Java Stream findAny() method can find a single element from the Stream. The element found can be from anywhere in the Stream. There is no guarantee about from where in the stream the element is taken.
Example:
Output
findFirst()
The Java Stream findFirst() method finds the first element in the Stream if any elements are present in the Stream. The findFirst() method returns an Optional from which you can obtain the element if present.
Example:
Output
forEach()
The Java Stream forEach() method is a terminal operation that starts the internal iteration of the elements in the Stream and applies a Consumer (java.util.function.Consumer) to each element in the Stream. The forEach() method returns void.
Example:
Output
min()
The Java Stream min() method is a terminal operation that returns the smallest element in the Stream.
Example:
Output
max()
The Java Stream max() method is a terminal operation that returns the largest element in the Stream.
Example:
Output
toArray()
The Java Stream toArray() method is a terminal operation that starts the internal iteration of the elements in the stream and returns an array of Objects containing all the elements.
Example:
Output
Conclusion
The Stream API is a powerful, but simple to understand set of tools for processing the sequence of elements. When used properly, it allows us to reduce a huge amount of boilerplate code, create more readable programs, and improve an app’s productivity.
In most of the code samples shown in this article, we left the streams unconsumed (we didn’t apply the close() method or a terminal operation). In a real java application, please don’t leave an instantiated stream unconsumed, as that will lead to memory leaks.
Comments