One of the most powerful features introduced in Java 8 was Lambda Even though at first it may not seem much, the new functionality speeds up both coding and execution in many cases, if used correctly. Here we will be looking over the power of Streams and Lambda expressions in Java and using them to do manipulations over collections. This is by no means an advanced tutorial, but an introduction to this functionality. Hopefully, some new information will be shared by the time you reach the end.
Initial class
We will start with a simple class for students that stores their name, age, and a few grades. This will help us in the examples later on.
public class Student {
private String name;
private double gradeAtMath;
private double gradeAtEnglish;
private int age;
public boolean passed() {
return (gradeAtMath + gradeAtEnglish) / 2 >= 5;
}
// Getters and Setters
}
Stream.filter() in Java
Let’s start with something small and easy. We have a collection (in this example a List, but it can be any Java collection) that stores the students in a university. Now, we want to extract those whose name starts with the letter “M”
List<Body> filtered= students.stream()
.filter(s -> s.getName().toUpperCase(Locale.ROOT).startsWith("M"))
.collect(Collectors.toList());
Let’s break this down. First, we create a Stream from our collection. Next, using filter() we provide a Lambda expression where we get the name of the student, making it upper case and checking if it starts with “M”. Last we collect the results in a new List.
At first, it may not seem much, since we can achieve the same effect using more traditional methods, however, we are only just getting started. We can chain multiple filters in order to do more complex operations without the need to write a big and fuzzy statement.
List<Student> filtered = students.stream()
.filter(s -> s.getName().toUpperCase(Locale.ROOT).startsWith("M"))
.filter(s -> s.getGradeAtMath() >= 5)
.filter(s -> s.getGradeAtEnglish() < 5)
.filter(Student::passed)
.collect(Collectors.toList());
Here we chain multiple filters to obtain the exact results we are looking for. An interesting example is the last filter, where we only need to specify the method that returns our value of interest, in this case, if he passed the year.
Do multiple filter() calls result in higher execution time?
This is a question that is asked many times and at first glance, it may seem that doing multiple filter() calls is not efficient. This, however, is not always the case. Depending on the Predicates used, there can be optimizations done by the compiler and the JVM that can result in the same or even better execution times. This is because for Streams, calling filter() twice does NOT mean the first call is executed on the original collection and the second one on the resulting collection. Streams have multiple pipelines that have intermediate and terminal operations. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.
One optimization that you can make in order to guarantee faster execution is to use method reference wherever possible instead of a lambda expression. If we use .filter(Student::passed) will yield less objects being created than .filter(s -> s.passed()) so in result faster execution times.
In most cases the execution time difference between a complex Predicate and chaining multiple filter calls, each with a simpler Predicate is negligible. My recommendation is to go for the one that is easier to read and understand.
Stream.sort()
Another useful function is to sort the collection. This is achieved using the sort() method and, just like for filter(), we can use Lambda expressions to simplify our work. Below, we will be sorting the students by name. Both examples do the same thing, but I provided them both to show how Lambdas and Comparators can be used to achieve cleaner code.
List<Student> sorted = students.stream()
.sorted((s1, s2) -> s1.getName().compareTo(s2.getName()))
.collect(Collectors.toList());
List<Students> sorted= students.stream()
.sorted(Comparator.comparing(Student::getName))
.collect(Collectors.toList());
In the second example we provide a Comparator that compares the return of the getName() method.
The power of the sorting functionality lies in the Comparator class. It is not uncommon for students to have the same name, so, let’s assume that when doing sorting by name we want to have the person with the highest grade at math before the other with the same name. In more traditional mechanisms this would be quite a complex task, especially if we would want to have even more criteria. In Java 8, with the power of the Comparator, this can be easily achieved by chaining multiple comparators and applying the resulted one to the sort() function.
| NAME | Grade at math | Grade at English | Age |
+------+---------------+------------------+------+
|Tom | 8 | 7 | 3 |
|Jerry | 9 | 5 | 2 |
|Spike | 7 | 9 | 4 |
|Tom | 7 | 7 | 3 |
And the code
List<Students> students = getAllStudents();
Comparator<Student> comparator = Comparator.comparing(Student::getName)
.thenComparing(Student::getGradeAtMath);
students.stream().sorted(comparator);
Output:
| NAME | Grade at math | Grade at English | Age |
+------+---------------+------------------+------+
|Jerry | 9 | 5 | 2 |
|Spike | 7 | 9 | 4 |
|Tom | 8 | 7 | 3 |
|Tom | 7 | 7 | 3 |
We can have as many calls to thenComparing() as needed. In real-life scenarios, where we have a lot of data, the chance of two or more entries having the same values for some of the fields is increased. Chaining comparators will allow us to have a precise order when such collisions exist.
This article was originally posted on my website. There you can find more information and examples on Java Streams and Collection Manipulation.
Top comments (0)