Getting started with Streams In Java

Sailee Renapurkar
4 min readDec 26, 2020

--

A few months back, I started to work on a client project. For any story I used to work on, I used to do it in a procedural way meaning using the loops, iterating, performing some operations, and then returning. Thank god we have a system of reviewing PR before pushing code to master. I started to get feedback on how to improve my code and to write it in a more functional way, using streams. I was very curious that a small piece of code, easily readable and understandable was much better than my bigger one. So I thought of writing this blog for the new engineering graduates to get familiar with the quite easy way of doing things. So let’s begin!

Before we start with all the concepts, let’s see how we used to iterate a list and calculate the sum of the numbers less than 50.

List<Integer> list = Arrays.asList(11,122,33,41,58);int sum = 0;Iterator<Integer> iterator = list.iterator();while (iterator.hasNext()) {int number = iterator.next();if (number< 50) {sum += number;}}

The above piece of code works totally fine, but we can see some issues with the code.

1. It is a pretty big code for calculating just the sum of numbers.

2. We need to think about using the correct way of iterating through the list.

3. We are describing a lot of what the code is doing i.e this is the descriptive way.

4. It is Sequential, There is no way to parallelize tasks with this approach.

In order to avoid these issues, Stream API was introduced which takes care of iteration internally. The above line can be shortened down to this:

int sum = list.stream().filter(number -> number < 50).sum();

So what are Java Streams?

A stream is an abstraction, it’s not a data structure. It’s not a collection where you can store elements. The most important difference between a stream and a structure is that a stream never holds the data. For example, we cannot point to the location in the stream where a certain element exists. We can only specify functions that can be used within the data in a stream in some order. A Stream is functional in nature, which means that it does not modify its source when producing the result. Many operations can be executed lazily promoting high performance.

So basically, java streams are nothing but a pipeline through which data flows and operates it. The stream API gives us the power to perform a sequence of operations on the data in a more declarative way, not a descriptive one. This is the beauty of functional programming that gives abstraction over data as well as functions. We do not need to worry about how to perform complex operations on data. To sum up, the pattern generally used while using the streams is :

1. Send a collection into a stream

2. Let the stream flow through different operations (i.e filter, map, etc).

3. Collect results back.

A stream can be obtained in many ways:

1. By calling stream() and parallelStream() methods on a collection.

2. Arrays.stream(Object[]) on an array etc.

Stream operations can be divided into two types:

1. Intermediate

2. Terminal

1. Intermediate Operations:

The intermediate operation returns a new stream, for e.g when we say filter(<predicate>), it doesn’t actually perform filtering, but it will create a new stream that will contain the elements of the initial stream that matches the given predicate.

Let’s see some of the examples of Intermediate operations.

a. filter():

Filter operation accepts a predicate to compute which elements should be returned in the new stream and removes the rest.

When doing it in a declarative way we need to write if and else. But in a functional way, we only work on the type of values we require i.e predicate.

e.g

List names = Arrays.asList(“abc”, “abd”, “aa”, “”, “bcd”, “acd”);List result = names.stream().filter(s->s.startsWith(“ab”) ).collect(Collectors.toList());

b. Map():

Map operation allows you to transform stream elements into something else. It accepts a function to apply to every element in the stream and returns the stream of values the parameter function has produced. It basically allows you to do computation on data inside the stream.

e.g

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8);List<Integer> result = list.stream().filter(num -> num % 2 == 0).map(num -> num*2).collect(Collectors.toList());

It is filtering the list for the even numbers and multiplying the resultant by 2.

2. Terminal Operations:

All operations that return something other than stream are called terminal operations.

Examples of some of Terminal Operations are:

a. collect():

collect() operation is simply used to collect the elements in a stream.

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8);List<Integer> result = list.stream().filter(num -> num % 2 == 0).map(num -> num*2).collect(Collectors.toList());

Here we are collecting the resultant stream into a new collection result.

b. anyMatch():

anyMatch() operation takes a predicate as an argument and returns boolean.

List<Integer> list = Arrays.asList(1,2,3);Boolean evenExist = list.stream().anyMatch(num -> num % 2 == 0);

evenExist will be true as there is an even number.

One of the best resources to dig deeper into the world of streams is the Javadoc of java. util.stream package.

Now that we understand what Streams are in Java 8, why should we use them instead of traditional loops? We should make an attempt to use streams wherever possible. The amount of clarity and simplicity of the syntax combined with ways for using powerful parallel reduction and a lot of other stream operations make them the better option in most of the cases compared to traditional loops.

--

--