Surely if you learned to program with C, C++, Pascal, or the first versions of Java and currently you are still dedicated to programming, you may have noticed that for some years now “strange” concepts related to functional programming have appeared, such as lambdas, it is possible that if you are of that “old school” you are not very happy with them because they seem complicated and unclear.
In this post we are going to try to remove that stigma to show that well used is a very powerful tool to reduce lines of code and effort, so let’s go there!
1. What is a lambda??
The first thing is to try to define what a lambda expression is. As we have mentioned a Lambda is a concept related to functional programming that was defined in Java 8 to try to provide Java with a functional paradigm that differs from the imperative programming paradigm.
2. What is Java Stream?
In version 8 of Java, an API was included that greatly facilitated the work with collections, it allows us to perform operations on the collection, such as search, filter, reorder, reduce, etc…
Throughout the article we will see some of the methods offered (although there are many others that you can use and that will surely be very useful).
3. My first lambda… “forEach”
The first lambda that we are going to use is one of the most basic and that surely many of you already know. It is the forEach method that we find in most of the classes that allow us to store collections (Iterable, Map…).
This method allows us to traverse the different collections in a simple way without the need to use the loops of imperative programming (for, while, do while…).
List<String> colors = Arrays.asList("Red","White","Black","Blue","Yellow");
colors.forEach(color -> {
cars.add(Car.builder().brand("Volvo").model("XC90").color(color).date(LocalDate.now())
.id(RandomStringUtils.random(7, true, true)).build());
});
In the code above we see how, starting from a list of colors, we can go through it using the forEach method and add a car with the color read in a list.
To use the forEach directly, it is enough to generate the following lambda structure.
colors.forEach (<variable> -> <actions>);
This will iterate through the list as many times as elements it contains and in it will introduce the value of that occurrence. That variable will have the data type that contains the list, in this case it is a String but it could be a compound data declared in our application.
This use of forEach is often very useful for traversing maps, as we can see in the following code fragment.
HashMap<String,String> map = new HashMap<>(); map.put("Volvo","XC90"); map.put("Seat","Leon"); map.put("Fiat","Punto"); map.put("Mercedes","CLA"); map.forEach((k,v)-> cars.add(Car.builder().brand(k).model(v) .color("White").date(LocalDate.now()) .id(RandomStringUtils.random(7, true, true)).build()) );
In this case we have a map of brands and models that we go through to add cars in our list. If we look at the difference with the previous example is that we can declare two variables at the beginning of the lambda, which correspond to the key and the value of the map.
map.forEach( (<key>,<value>) -> <actions>);
In this way we save a lot of code to traverse the map than if we had to traverse it using imperative programming, relying on the entrySet method to obtain the set of elements contained in the map, and the getKey and getValue methods.
Once we have seen one of the simplest ones, let’s start working with Stream, which is where I personally believe we get the most benefit.
4. Finding items with Stream
Let’s imagine that we want to check that certain cars exist or do not exist in the list we have generated with the previous examples.
If we wanted for example to check if there are gray cars in the list we would have two options. Through imperative programming, we could generate a findCar method.
private boolean findCar(List<Car> carList, String color){ for (Car next: carList){ if (next.getColor().equals(color)){ return true; } } return false; }
This method would receive the list of items and the color to filter, the method goes through the list to locate if there are items that meet that search criteria.
Once we have this method generated, it would be enough to invoke it as many times as we need it in the following way.
findCar(cars,"Grey");
Now let’s see how it would work when using Stream, in this case we could directly put the statement in the following format using lambdas.
cars.stream().anyMatch(car -> car.getColor().equals("Grey"));
This sentence searches if there is any record in the cars list whose color is equal to Grey returning a boolean.
As we can notice, in this last case we are avoiding the generation of many lines of code.
Stream offers us the possibility to find positive or negative, that is, to ask if an element exists (anyMatch) or if an element does not exist in the collection (noneMatch) the syntax would be the same.
stream().noneMatch(<record> -> <condition>)
cars.stream().noneMatch(car -> car.getColor().equals("Grey"));
In this case the <record> particle is analogous to what we have seen previously with the forEach method, it corresponds to the element of the list (in this case it is also our class and as we can see, we will be able to access directly to its methods).
The <condition> particle sets the condition we want to validate, in this case we have retrieved the color attribute to compare it with a specific color, but we could perform any condition or mixture of them.
For example, if we wanted to find all Volvo and White cars, we would only need to do the following:
cars.stream().noneMatch(car -> car.getColor().equals("Grey") && car.getBrand().equals("Volvo"));
In cases where the condition is complex, we can extract the condition to a new method and invoke it in the lambda statement.
private boolean applyCriteria(Car car) { return car.getColor().equals("Grey") && car.getBrand().equals("Volvo"); }
cars.stream().noneMatch(car -> applyCriteria(car));
This same process can be done with all the functions offered by Stream.
5. Filtering elements and additional operations with Stream
In addition to what we have seen so far, the Stream allows us to filter results from a list and perform certain operations on the items resulting from that filter.
For example, let’s imagine that we want to filter the cars that are white in order to apply a bonus on their insurance.
We could do this using the filter method.
cars.stream().filter(x -> x.getColor().equals("White")).forEach(car -> improveInsurance(car));
Using filter, we generate another Stream object that allows us to perform another function on it, in this case we have used the forEach to make a call to the method to perform the bonus.
It should be noted that we can perform many operations on the Stream object generated by the filter method:
- Count the number of elements obtained, using the count method
cars.stream().filter(x -> x.getColor().equals("White")).count();
- Generate a new list with the results obtained, in this case we will use the collect method
List<Car> result = cars.stream().filter(x -> x.getColor().equals("White")).collect(Collectors.toList());
- Generate a new list with the license plates of the cars we have filtered, for this we use the map method and then the collect method
List<String> ids = cars.stream().filter(x -> x.getColor().equals("White")).map(x -> x.getId()).collect(Collectors.toList());
In this case we are mapping the Id field through the getId method to a new list in which we will store only the license plates. This annotation that we have used in the map can be simplified with a reference to the method as follows Car::getId
List<String> ids = cars.stream().filter(x -> x.getColor().equals("White")).map(Car::getId).collect(Collectors.toList());
- Generate a new list eliminating duplicates, for example, let’s imagine that we want to obtain all the brands that have a white car, in this case it is possible that this filter will give us duplicate records, these records can be eliminated as follows
List<String> ids = cars.stream().filter(x -> x.getColor().equals("White")).map(Car::getBrand).distinct().collect(Collectors.toList());
- Sort the resulting filter using one of the fields of the class, in this case we will use the sorted method relying on the Comparator class in which we will establish which is the field on which we are going to perform the sorting, in this case we will also use the reference to the method
List<Car> result = cars.stream().filter(x -> x.getColor().equals("White")).sorted(Comparator.comparing(Car::getId)).collect(Collectors.toList());
In short, a lot of operations can be performed in a simple way using Stream, as we have mentioned these are perhaps the most basic, but there are other methods such as reduce, limit, flatMap, skip, peak,… that offer us more possible functionalities on the Stream object.
Conclusion?
It is true that some may think that by eliminating lines of code we are increasing the complexity of the SW and its maintenance, but in the end these are simple methods that can bring a substantial reduction in the time and lines needed to perform a functionality.
But if you are curious about this new programming model this can serve as a small appetizer.
Santander Global Tech is the global technology company, part of Santander’s Technology and Operations (T&O) division. With more than 2,000 employees and based in Madrid, we work to make Santander an open platform for financial services.
Are you a Programming expert and want to join this great team? Check out the positions we have open here and Be Tech! with Santander ?
Follow us on LinkedIn and Instagram.