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.