Lambda Expressions as Arguments

Remember one thing we discussed in the previous part while going through lambda expressions ? – This was the question “How do a filter operation accept a predicate to do the filtering and how do forEach consume or process each elements of the stream”.

We can achieve this with the help of lambda expressions. We can pass a lambda expression to filter which act as a predicate or we can pass a lambda expression to forEach method which tells how the stream elements should be consumed. But how do we pass a lambda expression as an argument to a method. Take a look at the below given example.

The above given example is similar to the previously discussed example. But this time we pass the lambda expressions which find the square and sum of numbers as an argument to a method. Here we have a Calculator class and it contains a static method calculate(). This method accepts an instance of CustomMath interface and an integer variable value as arguments. We already know CustomMath is a functional interface and it can be assigned with a lambda expression. So we can directly pass a lambda expression to calculate() method and the actual execution will be done from within the calculate() method.

Hope that is clear 🙂

The Problem

I have a list of objects which store details about the exam results of students from a batch. Each object store name of the student, a flag to tell if student has passed the exam or not and the total marks. I need to find students who passed the exam with total mark greater than 300 and I need to store and display each of the filtered students details.

Step 1 – Filter the list by checking the pass flag. Consider the record for next filter operation only if student is passed.
Step 2 – Filter the list by checking the total marks. Consider the record for next filter operation only if student got marks above 300.
Step 3 – Print out the name and total marks of passed students.

That is great 🙂

The Solution

Remember the Line 1 we discussed in the previous part of the series ? – Yes! we need to have two intermediate operations which do the filtering (Step 1 and Step 2) and one terminal operation which print each of the filtered record (Step 3).

How do we do that ?

Step 0 – Create a stream from the list of objects.

ArrayList students = new ArrayList();
students.stream()

Step 1 – Create a lambda expression which return true for when the first condition is met and pass this to the first filter method.

.filter((Student student) -> student.passed())

Step 2 – Create a lambda expression which return true when the second condition is met and pass this to the first filter method.

.filter((Student student) -> student.getTotalMarks() > 300)

Step 3 – Create a lambda expression which act as a consumer. Here, consuming means we are printing the name and total marks of each student present in the stream obtained from the last filter operation.

.forEach((Student student) -> System.out.println(student.getName() + " passed with " + student.getTotalMarks() + " marks."));

Note: Each of the above given lambda takes a Student instance as parameter and pass it to the right hand side. Now take a look at the complete code.

Filter Map and Reduction Operations…

Stream API provides a set of methods which are used frequently in stream operations. We already saw one such operation which is filter. But what are the other two operations ?

Map Intermediate Operation

The map operation is used to change the state of each elements present in the stream. Consider the example of students mark list processing. Say the school management has decided to add additional 10 marks to each of the students who are not passed. For this we can do the map operation.

students.stream()
 .filter((Student student) -> !student.passed())
 .map((Student student) -> student.addMarks(10))
 .forEach((Student student) -> System.out.println(student.getName() + " mark increased to " + student.getTotalMarks() + " marks."));

Here addMarks() must return the student instance as shown below

public Student addMarks(int marks) {
 this.totalMarks += marks;
 return this;
 }

Reduction terminal operations

Stream API provides a lot of terminal operations and some of them are termed as reduction operations. One of the reduction method supported by stream is the average method. Which needs to be called on a stream which have elements of type double. And such streams are called DoubleStream. Take a look at the below given example

OptionalDouble average = students.stream()
 .mapToDouble((Student student) -> student.getTotalMarks())
 .average();
 
 System.out.println("Average mark is: " + average.getAsDouble());

Here we are actually creating a stream which have real numbers as elements by calling the mapToDouble() method. The lambda expression used inside this method return the total marks of each of the students present in the stream. Once that stream is obtained we call the average() method to get the average.

Stream provide a method called reduce, which is a most useful reduction operation. Consider the above example and say i need to find the sum of total marks of student. We can do this by using a reduce operation. Take a look at the below given example.

OptionalDouble total = students.stream()
 .mapToDouble((Student student) -> student.getTotalMarks())
 .reduce((double x, double y) -> x + y);
 
 System.out.println("Total Marks: " + total.getAsDouble());

Unlike the the other methods discussed so far, the reduce() method accept a lamda expression which takes two arguments and return the sum of the two elements. Simply we can say that it is a binary operation. So the reduce method will return the sum of the elements present in the stream and return the result. The reduce method comes with different versions (I mean overloaded versions are available).

Collecting the Stream Elements…

We know that the operations on stream do not affect the actual data source connected to the stream obtained from a collection. What should we do if we need to save the filtered results for later processing instead of printing it on the go. Say we need to publish the results in a school website.

To do this Stream API provides a terminal operation called collect(). Which when called on a stream given by an intermediate operation return a collection. For example it can return a Set, a Map, or a List.

How do collect()return a collection ? – Java 8 provide a class called Collector which do have a set of static methods. Using this static methods we can specify how the collect() method can collect the elements from the stream. Say if the elements needs to structured as as a list we can use toList() method – which accumulates the elements of the stream as a list. Take look at the below given code

List passedStudents = students.stream()
 .filter((Student student) -> student.passed())
 .filter((Student student) -> student.getTotalMarks() > 300)
 .collect(Collectors.toList());
 
 for (Student student : passedStudents) {
 System.out.println(student.getName() + " passed with " + student.getTotalMarks() + " marks.");
 }

Hope that is clear 🙂

Stream Interface

Stream interface is a generic interface and so it can be used with all reference or data types. It adds several methods of its own and extends another interface called BaseStream. in the above example we saw filter, forEach methods and these are added by Stream interface. But how can we obtain a Stream instance from a collection ?

The Collection interface now includes two new methods stream() and parallelStream(). We already used stream() method which when invoked on a collection, will return a new stream by making the collection as a data source.

students.stream() // Obtain a stream from students collection
Stream studentStream = students.stream(); // Same as the above

Once we obtain the stream we can apply the intermediate and terminal operations.

Stateless and Statefull

The intermediate filter operations discussed above is considering one element at a time from the collection and the processing of that element doesn’t depend on any other elements from the collection. Say if “Student 1” is considered it doesn’t take other records in to account.

An intermediate operation is stateless if each of the individual elements are processed independently

So what is Statefull ? Say you want to sort the list of students based on the name. Definitely you cannot do it without considering all the students from the collection. If you take the first record, you need to have a look on other remaining records to place it correctly.

An intermediate operation is stateful if each of the individual elements depends on other elements from the collection

Sequential and Parallel Streams

The stream instance which is returned by the the stream method is actually a sequential stream. That means the stream operation is applied in each of the elements in sequential order. We can say it is is like a for loop execution. And this sequential stream execution will be done by utilizing a single core.

On the other hand the stream instance which is return by the parallelStream method will be utilizing multiple cores of the CPU. Say if there are ten filter operations to be performed on stream, these this will be divided among the available cores. In the case of sequential all will go to one single core. If we use parallel stream and sequential stream to print the data of a collection, and call a Thread.sleep(1000); each time you will get to see the difference between this two :).

THE END!

Thanks for reading. I believe you got an overview about the Java 8 stream API.
Feel free to add your thoughts, comments and suggestions 🙂

Advertisements