With Java 8, we were introduced to a new abstraction called Stream, using which we can process data in a declarative manner. This, when paired with the collector functions, make data processing much simpler. We can compute sums, averages, and other such properties. It also makes operations such as searching and sorting trivial. It also supports functions such as concatenation, filtering etc. As an added benefit, the stream abstraction has support for parallel computation in multi-threaded architectures, without the user being required to explicitly type it out. We shall look at some of these functions and how to use them below.

The Stream API

The stream API java.util.stream, is an important addition to Java 8. It is used to process sequences of elements easily. We can think of streams as a generalization of lists. Note that stream is not a separate data structure, but is rather a sort of data wrapper that can be used to access data. The data can be data from an array, collections, lists, sets etc. There are also separate streams for different primitive data types, such as IntStream, LongStream, DoubleStream which can be used as required. Stream operations can be either intermediate or terminal. Intermediate stream operations create another stream as an output and thus intermediate stream operations can be chained together. Terminal stream operations return either void or a non-stream object.

Creating Streams

We can create streams from arrays, lists and the like using stream() and of() as shown in the example below.

Using Streams

Streams make iterating over a sequence of values easier. We can iterate and do just about anything with streams in just one line of code. For example, if we want to check if a particular element “x” is present in a stream, we can use just one line of code as the following

We can also perform operations such as allMatch(), noneMatch() using similar code, except we have to replace anyMatch() with the match method we want to use. The following example shows this

Each of these lines of code returns a corresponding boolean value depending on the truth value of the statement.

Using similar code such as above, we can also perform many other operations that require iterations such as filter, match, etc. For example, the following line of code filters the elements that contain the char “x” and creates a new stream from these elements.

As stated above, since this returns another stream object, it is an intermediate operation and can be chained with other intermediate operations, which is useful in case we want to apply multiple filters.

We can also reduce the stream values into a smaller set of values, depending on our needs. This is done by calling the reduce() method. This method takes a start value, and an accumulator function as arguments, and applies the accumulator function iteratively to the stream starting from the start value, and outputs a final result. This is shown in the following example

Here, 15 is the start point and a+b is the accumulator function. We create a stream named digits, and use the reduce() method with the above parameters. The above code stores (15+1+1+1) = 18 in the integer reduced. As we can see, the start point need not be a part of the stream being processed.

Stream.collector() Method

Stream.collector() is a terminal stream method, that is, it operates on a stream object returns a non-stream object. Therefore, it is usually used as a final step in stream processing. We have to import the Collectors class to use it, which is generally done using a static import as the following

This imports the entire Collectors class. Alternatively, one can just import the individual collectors which are required.

We can then use a host of collector functions, which can be used to collect the stream in different forms.
For example, we can use the toSet() collector to collect a stream into a Set object. Say we are given a List named randomList. The toSet() collector is implemented as follows

We can implement toList(), and other collectors in a similar fashion. In case we want greater influence over the implementation of the collector, we can use toCollection() and then specify our preferred collection to store the stream in.
For example, say we want to store the stream of randomList in a Queue. Then we can achieve this by doing

We can write code in a similar fashion for different implementations of the Collections framework such as LinkedList, Stack, Deque, etcetera.

We can also use collectors for statistical manipulations. For example, say we have a List of numbers and we want to store their sum. One way is to use the reduce() method discussed above. We can also achieve this by using the summingInt collector. This is shown in the example below.

This takes a List represented by digits, converts it into a stream, and then uses the terminal method summingInt to collect the stream, sum the values and store them in an integer.

We also have predefined methods for other statistical values, such as computing averages, etc. In addition to these, there is also the summarizing Int/Double/Long() collector which can be used to get a statistical summary of the stream for a corresponding primitive data type. This returns a set that has the average, maximum value, minimum value, count, and the sum of the data in the stream. It is implemented as follows

Here, IntSummaryStatistics stores the aforementioned statistical summary of the data in the list digits.

As we have seen above, the Stream API, coupled with the Collector methods, is a powerful tool for manipulating and exploring data. A mastery of the Stream and Collector methods will, no doubt, make you into a master at data handling in Java. There are several other useful methods which we have not discussed, for which you can take a look at Java’s Stream Documentation.

LEAVE A REPLY

Please enter your comment!
Please enter your name here