November 24, 2018

Using java streams to make your java code lazy

What do I mean by making your code lazy?

Your code won’t do a single operation, until, output generated by it’s execution is not consumed by some other operation. It would look like the code is doing some action, but in reality it is not doing anything.

Let’s dig into this by using an example of looping over elements in a list of string.

This is a list of names. I am using integers in place of real names, because it is easier to get the length of string from the string itself, rather than counting the characters in your head for 5 minutes.

List<String> nameList = List.of("1", "12", "123", "12345", "123456", "123457" );

Let us loop over this list. We will go over the list and print the names with length greater than 4.

for(String name : nameList) {
    if(name.length() > 4) {
        System.out.println(name);
    }
}

Run it. And We get the following output.

12345
123456
123457

And, the output is correct.

Now, let me introduce a little boolean variable boolean printIt = true; And we will print the name only when, printIt is true.
Now, the modified code looks like this.

for (String name : nameList) {
   System.out.println("Filtering Operation on -> " +  name);
   if (name.length() > 4) && printIt) {
      System.out.println(name);
   }
}

Run it with printIt = false;

Output is :-

Filtering Operation on -> 1
Filtering Operation on -> 12
Filtering Operation on -> 123
Filtering Operation on -> 12345
Filtering Operation on -> 123456
Filtering Operation on -> 123457

If printIt is false, then name will not be printed. But, we are still iterating through the items in the list. It doesn’t matter, if we are not performing any operation with the items, the iteration still happens.

We can make a small change by wrapping the iteration in a boolean check.

if(printIt) {
     for (String name : nameList) {
      System.out.println("Filtering Operation on -> " + name);
       if (name.length() > 4) {
           System.out.println(name);
         }
      }
}

So, this way we can skip the iteration if we are not using it’s output.

Now, we will use java streams to attack the same problem.

Above code in java streams.

nameList.stream().filter((name) -> name.length() > 4).forEach(System.out::println);

Let us extract the predicate out of filter, that will help me demonstrate that the stream is lazy.

This is our extracted function, which returns true for the valid names.

private static boolean validNames(String name) {
  System.out.println("Filtering Operation on -> " +  name);
  return name.length() > 4;
}

Now, the code looks like this.

nameList.stream().filter(JavaLazyStreams::validNames).forEach(System.out::println);

Runt it and Output is,

Filtering Operation on -> 1
Filtering Operation on -> 12
Filtering Operation on -> 123
Filtering Operation on -> 12345
12345
Filtering Operation on -> 123456
123456
Filtering Operation on -> 123457
123457

So, all the valid names were printed and invalid ones were discarded.

Now, we are going to remove the forEach call .

nameList.stream().filter(JavaLazyStreams::validNames);

Run it. You won’t get any output.

Let us refactor this code for better readability.

//Variable to store streams
Stream<String> streamOfNames = nameList.stream();
//Variable to store vaid names
Stream<String> nameLengthGreaterThanFour = streamOfNames.filter(JavaLazyStreams::validNames);
//Print all the valid names
nameLengthGreaterThanFour.forEach(System.out::println);

In the above code,

  • We put stream in a variable. No evaluation happens. We are not iterating anything right now.
  • We put valid names in a list. But, they are not valid names at all, they are just a placeholder for an operation, which will execute sometimes in the future, if and only if
    it’s output is needed by another operation.
  • We call a terminal operation, forEach, which prints the names. This is the point where all streams and filter operations are triggered. No operation happened before this.

Run it. And output is the same as in the case of for-loop.

Filtering Operation on -> 1
Filtering Operation on -> 12
Filtering Operation on -> 123
Filtering Operation on -> 12345
12345
Filtering Operation on -> 123456
123456
Filtering Operation on -> 123457
123457

Now, Let me add  printIt = false; and wrap it in an if-statement.

if(printIt) {
    nameLengthGreaterThanFour.forEach(System.out::println);
}

Now, run the code. And there won’t be any output. Because, forEach  statement didn’t execute. It’s not about forEach call, streams won’t execute until they find a terminal operation.

Complete code looks like this,

public static void main(String[] args) {
    boolean printIt = false;
    List<String> nameList = List.of("1", "12", "123", "12345", "123456", "123457" );

    Stream<String> streamOfNames = nameList.stream();

    Stream<String> nameLengthGreaterThanFour = streamOfNames.filter(JavaLazyStreams::validNames);

    if(printIt) {
       nameLengthGreaterThanFour.forEach(System.out::println);
    }
}

private static boolean validNames(String name) {
    System.out.println("Filtering Operation on -> " +  name);
    return name.length() > 4;
}

Although, validNames  is called inside the filter method, it didn’t print anything to the console.

Hence, Java streams are lazy, Proved.

 

 

Leave a Reply

Your email address will not be published.