Simple Collection Manipulation in Java Using Lambdas
One of the most powerful features introduced in Java 8 was Lambda expressions. 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 an 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 the 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 |
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.
distinct()
If you want to remove duplicates from a collection, Streams offer an easy to use API as well. Calling .distinct() will result in a new Stream that contains distinct objects. The distinction is done according to Object.equals(Object), so this means we can easily eliminate elements that are identical from a content’s point of view, even if they are different references.
List names = Arrays.asList("Tom", "Jerry", "Spike", "Tom", "Cousin");
names.stream().distinct().collect(Collectors.toList())
; // -> [Tom, Jerry, Spike, Cousin]
findFirst() and findAny()
Once we have done our filtering and sorting we may want to find the first element from the resulting Collection. This can be done by calling findFirst(). Keep in mind that it will return an Optional and you will have to check if it is empty or not. Your filters may result in an empty collection so findFirst() may not return an element.
If we don’t care which element we receive, findAny() is the way to go. Keep in mind though that there are no guarantees on the returned element and multiple calls on the same stream may return different results. This is because findAny() is optimized for parallel performance.
Also, remember that both methods are terminal operations and will result in the actual traversal of the stream.
Bonus: allMatch(), anyMatch() and noneMatch()
Using the Stream of a collection we can easily find if there are any matches for a certain condition. This is similar to filtering of the elements, but it will only return a Boolean value depending on the Predicate used. As the name of the method suggests, we can determine if all the elements match the condition, if any of the elements or if none of them. The below example assumes the input is the table from above.
students.stream().allMatch(s -> s.getAge() < 10); // returns true
students.stream().anyMatch(s -> s.getName().toUpperCase(Locale.ROOT).startsWith("Tom")); // returns true
students.stream().noneMatch(s -> s.getName().toUpperCase(Locale.ROOT).startsWith("L")); // returns true
Conclusions
The Stream API when used properly offers great performance and will result in easier to maintain and read code. Combining different operations like filter() and sort() will simplify work and will make it easier to focus on what really matters, without the need to pay too much attention on optimizing the conditions and sorting algorithms yourself.
There are even more useful functions that will help with collection manipulation and are as easy to use and understand as the ones mentioned in this article. I recommend you look over them and experiment with what Streams and Lambdas have to offer.
1 Response
[…] Learn hot to use Lambda expression in Java 8 Stream to easily filter, sort and manipulate collections like Lists and Sets. Read more […]