Java - lambdas and streams, or a few words about functional programming

Rafał Pieńkowski
Calendar icon
17 marca 2020

Java 8 was already released a good few years ago, and the streams it introduced make java programming easier for developers almost every day. So it is worthwhile for you to get well acquainted with them.

The Stream API uses lambdas, so let's start by explaining what these "lambdas" are and what they are for. In simple terms, they are anonymous methods, i.e. methods that do not belong to any class, but whose definition we write right at the place where they are called. We'll see this in a moment with an example, but before we do, let's create a simple Person class, which will also serve us later in the article.

1public class Person { private String name; private int age; // constructors, getters and setters

We have a simple Person class in which we have name and age fields. For clarity, let's assume that constructors, getters and setters are defined. Suppose we have a list of such persons, and then, we want to sort this list by name:

1List<Person> persons = new ArrayList<>(); persons.add(new Person("Andrew", 30)); persons.add(new Person("Stefan", 26)); persons.add(new Person("Catherine", 29)); Collections.sort(persons, new Comparator<Person>() { @Override public int compare(Person p1, Person p2) { return p1.getName().compareTo(p2.getName()); } });

The sort method takes an object of type Comparator as the second argument, so in this case we create an anonymous class. Before java 8, you had to write like the example above - a lot of "boilerplate code," that is, code required by the compiler, but contributing nothing to the business logic. In fact, only:

1p1.getName().compareTo(p2.getName());

Is the business logic here. With the help of lambdas come:

1Collections.sort(persons,(p1, p2) -> p1.getName().compareTo(p2.getName()));

Let's discuss the individual elements:(p1, p2) is a set of arguments. As you can see, there is no need to define types - the compiler knows about them. -> is a syntax symbol, separating the arguments from the body of our lambda. Then there is the body of the lambda - if you can write it in one line, both curly brackets and the return word are unnecessary.

Once we know the lambdas, let's move on to the "meat," or Stream API. This is most often used when we want to filter or transform a given collection, using easier and more readable code than standard nested loops. Let's extend our initial Person class:

1public class Person { private String name; private int age; private List<String> pets; // constructors, getters and setters }

We have added a list of the names of the person's pets.

Suppose, having a list of such persons, we want to get a list of such persons who are less than 30 years old:

1List<Person> personsUnder30Age = persons.stream() .filter(person -> person.getAge() < 30) .collect(Collectors.toList());

Let's analyze step by step what is happening:

  • .stream() creates an object of type Stream. This is how we build a stream from a collection.
  • .filter(...) is a method that operates on the stream, which filters its elements and passes on only those that meet the given condition, i.e. the method body returns true. This method accepts the stream and also returns the stream. If we wanted to add another method operating on the stream, we could call it directly after using .filter(...).
  • person -> person.getAge() < 30 is a filter condition, written as a lambda
  • .collect(Collectors.toList()) is a terminating operation that turns the resulting stream into a given collection, in this case, a list.
  • personsUnder30Age is the resulting list. It is a completely new list, i.e. the persons list remains unchanged.

Suppose that having a list of persons, we want to get a list of their names. In other words, we want to neatly transform objects of type Person into a list of Strings. This will be realized by the following code:

1List<String> names = persons.stream() .map(person -> person.getName()) .collect(Collectors.toList());

Here we see similarities to the example with filtering, but the difference is the use of the .map(...) method instead of .filter(...). It performs a mapping - in this example, it transforms a stream of elements of type Person into a stream of elements of type String (according to the use of the getName() method of the Person object).

Another example, would be the situation of converting a list of people into a list of animal names - for example, we want to know all the available animals in a given group of people (so, for example, "dog", "cat", etc.). At first glance, we could start with the following lambda:

1.map(person -> person.getPets())

However, as we will see, instead of a stream of elements of type String, we will get a stream of elements of type **List

**! This is not what we wanted, so we need to "flatten" this structure somehow. With help comes the .flatMap(...) method:

1Set<String> petNames = persons.stream() .map(person -> person.getPets()) .flatMap(pets -> pets.stream()) .collect(Collectors.toSet());

So, we use the mapping normally as we want, but later, there is a need to flatten the stream - thanks to this confluence, later we are already operating on the String stream. But beware - different people may have had the same animals, so to avoid duplicates, we should use a set this time instead of a list.

Streams in java allow you to write readable code quickly, but keep in mind that they introduce some overhead, also for very strict timing requirements you may find that staying with standard loops will be more efficient. In the above article, I presented the 3 most commonly used stream operation methods, i.e. .map(...), .filter(...) and .flatMap(...). Of course, the Stream API is much richer, so I encourage you to explore it and try different combinations.

Read also

Calendar icon

27 wrzesień

Omega-PSIR and the Employee Assessment System at the Warsaw School of Economics

Implementation of Omega-PSIR and the Employee Evaluation System at SGH. See how our solutions support university management and resea...

Calendar icon

12 wrzesień

Playwright vs Cypress vs Selenium: which is better?

Playwright, Selenium or Cypress? Discover the key differences and advantages of each of these web application test automation tools. ...

Calendar icon

22 sierpień

A new era of knowledge management: Omega-PSIR at Kozminski University

Kozminski University in Warsaw, one of the leading universities in Poland, has been using the Omega-PSIR system we have implemented t...