A In-Depth guide to Java 8 Stream API

In this post, we will see an in-depth overview of Java 8 streams with a lot of examples and exercises.

Introduction

You may think that Stream must be similar to InputStream or OutputStream, but that’s not the case.

A Stream represents a sequence of elements supporting sequential and parallel aggregate operations. Stream does not store data, it operates on source data structures such as List, Collection, Array etc.

Most stream operations accept functional interfaces that make it a perfect candidate for lambda expressions.

If you are not well versed with functional interfaces, lambda expressions, and method references, you may want to read the following tutorials before moving ahead.

Types of Stream operations

There are two types of Stream operations.

  1. Intermediate operations: return a stream that can be chained with other intermediate operations with dot .
  2. Terminal operations: return void or non stream output.

Let’s understand with the help of simple example.

Output:

JOHN
MARTIN
MARY
STEVE

Here,
To perform a computation, stream operations are built into a Stream pipeline. Stream pipeline consists of:

  1. source
  2. zero or more intermediate operations
  3. terminal operation.

In our example, Stream pipeline consists of:
Source: stringList
1 Intermediate operation: Map
1 terminal operation: forEach

The below diagram will make it more clear.
map is intermediate operation and foreach is terminal opertion.
StreamBasic

Most stream operations accept parameters as that describes user-defined behaviour, 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 pipline. You can learn more about interference.
Stateless: In most cases, lambda expressions should be stateless. Its output should not depend on state that might change during execution of Stream pipeline. I have already covered stateful lambda expression in Parallel Stream tutorial.

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.

First parameter of iterate method represents first element of the Stream. All the following elements will be generated by lambda expression n->n+1 and limit() is used to convert infinite Stream to finite Stream with 5 elements.

Lazy evaluation

Streams are lazy; intermediate operation are not executed until terminal operation is encounterd.

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.

Let’s understand with the help of example:

In preceding output, you can see that unless and until terminal operation count is called, nothing was printed on console.

In the preceding example, peek() method is used to print the element of stream. peek() method is generally used for logging and debugging purpose only.

Order of operations

Let’s see how stream processes the order of operations.
Could you guess output of the program?

Output will be:

Map: mohan
Filter: MOHAN
Map: john
Filter: JOHN
JOHN

Here order of operations might be surprising. A common approach will be to perform intermediate operation on all elements and then perform next operation, but instead each element moves vertically.

This kind of behavior can reduce actual number of operation.
For example:
In preceding example, Strings vaibhav and amit did not go through map and filter operation as we already got result(findAny()) with String john.

Some of the intermediate operations such as sorted are executed on the entire collection. As succeding operations might depend on the result of sorted operation.

Primitive Streams

Apart from regular Stream, Java 8 also provides primitive Stream for int, long and double.
Primitive Streams are:

  1. IntStream for int
  2. LongStream for long
  3. 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.

Here is an example of an IntStream.

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.
Here is an 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.
Here is an example:

Employee class

Consider a Employee class which has two fields name, age, listOfCities.

Here listOfCities denotes cities in which Employee has lived so far.

This Employee class will be used in all succeeding examples.
Let’s create employeesList on which we are going to perform intermediate and terminal operations.

Common intemediate operations

Map()

Map() operation is used to convert Stream<T> to Stream<R>. It produces one output result of type 'R' for each input value of type 'T'. It takes Function interface as parameter.
For example:
You have stream of list of employees and you need a list of employee names, you simply need to convert Stream to Stream.

StreamMap
Logical representation of Map operation

You can also use map even if it produces result of same type.
In case, you want employee name in uppercase, you can use another map() function to convert string to uppercase.

Filter()

Filter() operation is used to filter stream based on conditions. Filter method takes Predicate() interface which returns boolean value.
Let’s say you want to employees whose name starts with ‘A’.
You can write following functional code to achieve the same.

StreamFilter
Logical representation of Filter operation
[![StreamFilter]

sorted()

You can use sorted() method to sort list of objects. sorted method without arguments sorts list in natural order. sorted() method also accepts comparator as parameter to support custom sorting.

💡 Did you know?

Natural order means sorting the list based on comparable interface implemented by list element type.
For example:
List will be sorted on the basis of comparable interface implemented by Integer class.

Here is the sorted() method example

Here is the sorted() method example with Comparator as a parameter.

You can also rewrite this with method reference as below:

limit()

You can use limit() to limit the number of elements in the stream.
For example:
limit(3) returns first 3 elements in the list.

Let’s see with the help of an example:

Skip()

skip(int n) method is used to discard first n elements from the stream.
For example:
skip(3) discards first 3 elements from stream.

Let’s see with help of example:

flatmap()

map() operation generates one output for each input element.

What if you want more than one output for each input?
flatmap() operation is exactly used for this purpose. It is used to map multiple-output for each input.
For example:
We want to accumulate list of cities in which all employees have lived. One employee could have lived in multiple cities so that we may have more than one city for each employee.

Let’s see with help of example:

Common terminal operations

foreach

foreach() is terminal operation which is used to iterate over collection/stream of objects. It takes consumer as a parameter.

Let’s say you want to print elements of the stream.

collect

collect() is terminal operation which performs mutable reduction on the elements of Stream using Collector. Collectors is utility class which provides inbuilt Collector.
For example:
Collectors.toList() provides a Collector which converts Stream to a list object.
Following code accumultates Employee names into a Arraylist

Reduce

The reduce operation combines all elements of Stream and produces single result.
Java 8 has three overloaded version of reduce method.

  1. Optional<T> reduce(BinaryOperator<T> accumulator):
    This method takes BinaryOperator accumulator function. BinaryOperator is BiFunction where both the operands are of same type. First parameter is result till current execution, and second parameter is the current element of the Stream.

    Let’s find name of Person with minimum age.

  2. T reduce(T identity, BinaryOperator accumulator):
    This method takes identity value and accumulator function. identity value is initial value of the reduction. If Stream is empty,then identity value is the result.
    Let’s find sum of all ages of Employees

  3. <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner):
    This method takes identity value and accumulator function and combiner. Combiner is mainy used in case of Parallel Streams. Combiner comibnes the result of sub-stream that run in Parallel.

count

count() is used to count number of elements in the stream.

allMatch()

allMatch() returns true when all the elements in the stream meet provided condition.

This is a short-circuiting terminal operation because operation stops as soon as it encounters any unmatched element.

nonMatch()

nonMatch() returns true when all the elements in the stream do not meet provided condition.

This is a short-circuiting terminal operation because operation stops as soon as it encounters any matched element.

anyMatch()

anyMatch() returns true when any element in the stream meets provided condition.

This is a short-circuiting terminal operation because operation stops as soon as it encounters any matched element.

min()

min(Comparator) returns minimum element in the stream based on the provided comparator. It returns an object which contains actual value.

max()

max(Comparator) returns maximum element in the stream based on the provided comparator. It returns an object which contains actual value.

Parallel Streams

You can create Parallel Stream using .parallel() method on Stream object in java.
Here is an example:

Here is a comprehensive article on Java 8 Parallel Stream.

Exercises

Let’s practice some exercises on Stream.

Exercise 1

Given a list of employees, you need to find all the employees whose age is greater than 30 and print the employee names.(Java 8 APIs only)
Answer:

Exercise 2

Given the list of employees, find the count of employees with age greater than 25?
Answer:

Exercise 3

Given the list of employees, find the employee whose name is John.

Exercise 4

Given a list of employees, You need to find highest age of employee?
Answer:

Excercise 5

Given a list of employees, you need sort employee list by age? Use java 8 APIs only
Answer:

Excercise 6

Given the list of Employees, you need to join the all employee names with ","?
Answer:

Excercise 7

Given the list of employees, you need to group them by name

Was this post helpful?

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *