Introduction
Java 8 launched one of its best features: lambda functions - the first step of Java towards functional programming. Basically it means that functions can be created without a class, and also can be passed around through methods.
Lambda functions help quite a lot when dealing with collections. Instead of going through loops in order to manipulate their values, lambda functions provide a quick, and a much more readable manner to manipulate them. Going through all the details as how they work, would take much more than one blog post. Therefore here, it will be shown the most common used functions, at least in my humble experience.
Filtering data
By far, the must useful task one could do with a collection is filter it. And this could be accomplished via the following code:
List<Integer> ints = Arrays.asList(12, 32, 1, 3, 5, 3, 10, 8, 100);
List<Integer> filteredElements = ints.stream().filter(element -> element > 10)
.collect(Collectors.toList());
As one can observe, the code is quite straightforward: the ints
list of integers is created, then the stream()
method is called on it. This provides the possibility of using functional programming, already built-in. Here the filter(...)
method is used, and it does the following:
- for each element in the collection of
ints
, it applies a boolean function. In this case it checks whether each item is higher than 10:element > 10
. When this is true, the element is passed on to the handled by the next functional method.
Next, the .collect(Collectors.toList())
function is applied, which transforms the result of the filter(...)
into a list of Integers, represented by the variable: filteredElements
. Below is the result of printing the resulting collection (helper methods are used to nicely print the results in the article, but they are not listed, because they are quite simple):
Filter Integers higher than 10: [12, 32, 100]
Note how easier and explicit is the usage of the filter(...)
method. Only by a first glance, the developer can understand the purpose of the method. This is much easier to read than the interaction through a collection of integers, and the addition to a new list only the elements which are higher than 10. Also, it is less verbose.
The filter method can also be applied to object parameters, as displayed below:
List<Person> peopleList = Person.createPersonList();
List<Person>
filteredPersonList =
peopleList.stream().filter(person -> person.getAge() > 30).collect(Collectors.toList());
Here it is filtered from a list, only elements of the object Person
which has the age
parameter higher than 30. The object person is defined below (observe that the function createPersonList()
is also displayed):
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isPersonCool() {
return this.age > 40;
}
public static List<Person> createPersonList() {
List<Person> personList = new ArrayList<>();
personList.add(new Person(18, "Little person"));
personList.add(new Person(25, "Young adult"));
personList.add(new Person(33, "Responsible adult"));
personList.add(new Person(45, "Almost grandfather"));
personList.add(new Person(60, "Mein Opa"));
return personList;
}
}
Still on the subject of filtering, one can apply any boolean
function to apply as a filter
. Here, the function isPersonCool
(implemented above) will be applied:
List<Person> peopleList = Person.createPersonList();
List<Person>
filteredPersonList =
peopleList.stream().filter(Person::isPersonCool).collect(Collectors.toList());
Note how the notation Cass::function
can be used to simplify the notation, when the boolean function is applied as Person::isPersonCool
. This improves readability and leaves the code cleaner. Since someone cool is a person older than 40, the result of the function above is:
Filter by cool people: [Person{age=45, name='Almost grandfather'}, Person{age=60, name='Mein Opa'}]
Mapping data
Another useful task for lists, is to map from one object to another. Such activity can also easily be accomplished using lambda functions. In the next example, a list of Person
objects, will be mapped to Superhero
objects. Below is the Superhero
class implementation:
public class Superhero {
private int age;
private String name;
private String superPower;
public Superhero(int age, String name, String superPower) {
this.age = age;
this.name = name;
this.superPower = superPower;
}
@Override
public String toString() {
return "Superhero{" +
"age=" + age +
", name='" + name + '\'' +
", superPower='" + superPower + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSuperPower() {
return superPower;
}
public void setSuperPower(String superPower) {
this.superPower = superPower;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
And here is the map(...)
usage example:
List<Superhero> superheroes = personList
.stream()
.map(person -> new Superhero(person.getAge(), person.getName(), "fly")).collect(
Collectors.toList());
In the code above, here is what happens:
- for each instance of the
Person
object, a new instance ofSuperhero
is created, with the parametersage
andname
from the relatedPerson
, and with the third parameterfly
as a superpower. - Each instance of
Superhero
is then passed to the next lambda function to be worked. - Then, the function
collect(Collectors.toList())
is applied. It gathers the result of the previous transformation (mapping) and put into aList
ofSuperhero
objects.
The result of the transformation, is depicted below:
Map ordinary people to superheroes: [Superhero{age=18, name='Little person', superPower='fly'}, Superhero{age=25, name='Young adult', superPower='fly'}, Superhero{age=33, name='Responsible adult', superPower='fly'}, Superhero{age=45, name='Almost grandfather', superPower='fly'}, Superhero{age=60, name='Mein Opa', superPower='fly'}]
Filtering and mapping data
So far filtering and mapping of data have been applied separately. Nothing forbids, however, for them to be used together, as the example below shows:
List<Superhero> coolSuperheroes =
Person.createPersonList().stream()
.filter(Person::isPersonCool)
.map(coolPeople -> new Superhero(coolPeople.getAge(), coolPeople.getName(), "be invisible"))
.collect(Collectors.toList());
In the example above, a list of People
are filtered by the criteria, of who is cool, by the usage of the function Person::isPersonCool
. Later, all cool people are transformed into superheroes, by the usage of mapping: .map(coolPeople -> new Superhero(coolPeople.getAge(), coolPeople.getName(), "be invisible"))
. The final step is to collect the list of Superhero
objects via .collect(Collectors.toList())
. The result of the discussed code can be seen below:
Filter by coolness and map to superheroes: [Superhero{age=45, name='Almost grandfather', superPower='be invisible'}, Superhero{age=60, name='Mein Opa', superPower='be invisible'}]
Gathering data
Making arithmetic with collections data is another common activity in a collection. Of course, this is also supported by lambda functions.
Below is an example where all elements of an integer list are summed:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
int sumResult = integers.stream().mapToInt(Integer::intValue).sum();
In the above example, firstly the list of integers
calls the stream()
method, in order to have access to the lambda functions. Then, each element is mapped to an integer. By doing so, one has access to the sum()
function, which, as its name suggests, sum all elements in the mapped function. The result is shown below:
Sum ints using IntStream.sum(): 45
One interesting function which one can use to sum all elements in a list, is the reduce
. It does the following:
- for a list
l
, there is an accumulator valuea
. Each element of the list is callede
.- a reduction function
r
is applied to each elemente
. The functionr
has as its first parameter the accumulatora
and the current value of the liste
. The reduction function has as a returning value, the new accumulatora
which is the result of whichever algorithm was used on the reduction functionr
.
- a reduction function
Indeed the definition above is quite generic, and hard to grasp. Therefore, see the example below, which will make things much more clearer:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
int sumResult = integers.stream().reduce(0, (a, e) -> a + e);```
{% endraw %}
Note the reduce function {% raw %}`r`{% endraw %} sums the previous accumulator value {% raw %}`a`{% endraw %} with the current element {% raw %}`e`{% endraw %}. So, for each element of the list, its value is summed to the total already calculated before. The initial value for the accumulated value {% raw %}`a`{% endraw %} is zero, as defined here: {% raw %}`reduce(0, (a, e) -> a + e)`{% endraw %}.
Other interesting functions are provided, such as calculating the average of a list of numbers:
{% raw %}
```java
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
OptionalDouble meanResult = integers.stream().mapToInt(Integer::intValue).average();
Calculating the maximum value of a list:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
OptionalInt maxResult = integers.stream().mapToInt(Integer::valueOf).max();
And of course, calculating the minimum:
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
OptionalInt minResult = integers.stream().mapToInt(Integer::valueOf).min();
Filtering, mapping and gathering data altogether
As expected, all the three types of lambda functions discussed can be put together, as in the example below:
OptionalDouble averageAge = Person.createPersonList().stream()
.filter(Person::isPersonCool)
.map(coolPeople -> new Superhero(coolPeople.getAge(), coolPeople.getName(), "be invisible"))
.mapToInt(Superhero::getAge)
.average();
Here, a collection of Person
objects, is filtered by coolness. Next, they are mapped to a list of Superhero
objects, for then having the average of their age calculated.
Discussions
Please note that here, not all lambda functions were discussed, only the most common ones. Of course, readers may disagree of what is more common, or useful. Regardless, it will be hard to find a developer who does not find those quite useful. Therefore, if you are new to this topic, this would serve as quick and nice starting point.
Moreover, observe how easy it is to understand the meaning of each one of them. Having names describing what exactly is happening, is much readable than to have loops and if
clauses within them to accomplish the same goal.
You can find the entire source code used in this article here.
Top comments (0)