Java Stream API

Chilanka S.Halpage
5 min readJun 14, 2021

--

Overview

Java stream API comes with Java 8 and it was one of the major features added to Java 8. In this blog, we will focus on some features of Java stream API with simple examples. As prerequisites, you should have some knowledge of lambda expression, functional interface, method reference, and constructor reference.

You could use stream with Java by importing java.util.stream package. A stream is a wrapper around a data source and by which we can operate a bunch of processes on that data source fast and conveniently. A stream represents a sequence of objects from the source, which supports aggregate functions. A stream does not store data (i.e. not a data structure) and it never modifies the underlying data source.

Before we focus on stream further, you should know about the following terminologies.

Pipeline — A stream pipeline has got a stream source, followed by zero or more intermediate operations, and a terminal operation.

Source — resource, taken by a stream as input. (That could be array, list, set, etc.)

Intermediate operation — operations such as filter() return a new stream on which further processing can be done

Terminal operation — after the operation is performed, the stream pipeline is considered consumed, and can no longer be used. operations, such as forEach(), mark the stream as consumed, after which point it can no longer be used further.

Aggregate operation — operations such as map, filter, reduce supported by a stream. (These operations will be discussed later in this tutorial)

Note: You will understand the above terminologies more clearly while we are going ahead.

Streams are lazy because intermediate operations are not invoked until terminal operations are invoked. Using an example with findFirst() terminal operation, we will discuss this later.

Creation of stream

A couple of ways are available to create streams and some of which are using arrays, lists, and stream builder.

(Student class, whose fields are id, name, and age, is going to be used with examples)

If necessary, you can create empty streams as well.

Stream operations

forEach()

forEach() operation is mostly used and is a terminal operation. It loops on each element in the stream(i.e. iterates on each element). This behaves like a for loop

filter()

filter() produces a new stream and is used to eliminate the elements in the input stream based upon the condition given to the predicate interface. This operation is an intermediate operation. The below example shows filtering students whose age is greater than 25 using filter()

collect()

collect() can be used to collect elements in the stream into a data structure like list. This is a terminal operation

The below example shows the way of using forEach(), filter() and collect()

map()

map() produces a new stream as the output from the input stream by applying a function to elements in the input stream. The map() operation is an intermediate operation. The type of new stream could differ from the type of input stream.

The below example converts the Student type stream into an Integer type stream. map() gets the age of every student from the input stream and adds 10 to it. Thereafter, it returns an Integer type stream, which is converted to a list through collect().

flatMap()

flatMap() is used to flatten complex data structures in order to simplify it. Then it would be easier for further operation. (complex data structure => Stream<List<String>>, after simplifying => Stream<String>). This is an intermediate operation.

findFirst()

findFirst() produces an element of type optional for the first entry in the stream. If not found any entry, empty optional will be returned. This is a terminal operation. The below example illustrates the use of findFirst() and the lazy evaluation of the Java stream using findFirst().

You can see in the above example methodTest() is called only twice. Because 30, which is the age of the second student, satisfies the condition of the filter and passes it to terminal operation. Since we need only the first element, the student, whose age is 27, is not evaluated. The above example illustrates that intermediate operations are lazy and source elements are consumed only if they are needed.

toArray()

This terminal operation can be used to collect data elements into an array from the stream.

peek()

It is like forEach() operation. But peek() is an intermediate operation whereas forEach() is a terminal operation. peek() returns a stream. If you require to perform an operation on each element of the stream and return a new stream, peek() could be used.

sorted()

This is used to sort the elements in the stream the way of ascending or descending based upon the comparator passed. sorted() is an intermediate operation.

min() and max()

These two functions return min value and max value respectively in the stream based on the comparator. These two are terminal operations and return an optional.

distinct()

This non-argument function is used to obtain distinct elements in the stream (i.e. eliminates duplicates). This is an intermediate operation.

allMatch(), anyMatch() and noneMatch()

These all operations take a predicate as parameters and return a boolean value. The predicate may not be evaluated on all elements in the stream unless it needs to determine the result. Furthermore, all operations are terminal operations.

allMatch() returns true if either all elements match the given predicate or the stream is empty. After the first element, which does not match the given predicate, is found, no further processing is done and return false.

anyMatch() returns true if any one element in the stream match the given predicate. After the first element, which matches the given predicate, is found, further processing on the rest of the elements is not done and return true.

noneMatch() returns true if either no elements in the stream match the given predicate or the stream is empty. After the first element, which matches the given predicate, is found, further processing on the remaining elements in the stream is not done and return false.

IntStream, LongStream and DoubleStream

These are specialized streams for primitive data types. (i.e. int, long and double). When we deal with numbers or primitive types, using these streams is easier. Some of the operations in the stream are not supported by these specialized streams. Even if min(), max() in stream take a comparator, these specialized streams do not take a comparator. Moreover, we can use some specialized operations such as sum(), average() with these streams.

You could use IntStream.of() or mapToInt() to create IntStream. As same, LongStream and DoubleStream can be created using LongStream.of() or mapToLong() and DoubleStream.of() or mapToDouble() respectively.

If any unconsumed stream is in the code, it is better to use close() since it result in leaking memory

Conclusion

Stream API is a powerful feature in java and easy to understand. It allows reducing a huge amount of boilerplate code. When it is used properly, it increases readability and productivity.

--

--

Chilanka S.Halpage
Chilanka S.Halpage

Written by Chilanka S.Halpage

0 Followers

Undergraduate at Faculty of Information Technology, University of Moratuwa

No responses yet