DEV Community

Cover image for My most used Java 8 Lambda functions
Eduardo Antonio Cecilio Fernandes
Eduardo Antonio Cecilio Fernandes

Posted on

My most used Java 8 Lambda functions

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());
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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'}]
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

And here is the map(...) usage example:

List<Superhero> superheroes = personList
        .stream()
        .map(person -> new Superhero(person.getAge(), person.getName(), "fly")).collect(
        Collectors.toList());
Enter fullscreen mode Exit fullscreen mode

In the code above, here is what happens:

  • for each instance of the Person object, a new instance of Superhero is created, with the parameters age and name from the related Person, and with the third parameter fly 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 a List of Superhero 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'}]
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

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'}]
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 value a. Each element of the list is called e.
    • a reduction function r is applied to each element e. The function r has as its first parameter the accumulatora and the current value of the list e. The reduction function has as a returning value, the new accumulator a which is the result of whichever algorithm was used on the reduction function r.

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 ifclauses within them to accomplish the same goal.

You can find the entire source code used in this article here.

Top comments (0)