Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

When we work with Java, sometimes we want to generate a list from another list of objects. Java 8 introduced a set of new features that streamline such operations.

So, in this tutorial, we’ll explore how to create a list of objects of a different type based on a given list, using the powerful features introduced in Java 8 and beyond.

2. Introduction to the Problem

As usual, let’s understand the problem through examples.

Let’s say a company is looking to kickstart an internal tennis tournament. Now, the tournament committee wants a player candidate list from all company employees. So, we’ll take this task and create a program to build up the player candidate list.

The Employee class is ready:

@Getter
class Employee {
    private final String name;
    private final Set<String> hobbies = new HashSet<>();
    private final String email;
    private String department;
    // ... other attributes

    public Employee(String name, String email, Collection<String> hobbies) {
        this.name = name;
        this.email = email;
        this.hobbies.addAll(hobbies);
    }
}

As the code above shows, we use Lombok‘s @Getter annotation to make the Employee class have getter methods for all properties.

Each Employee object carries the hobbies Set, which holds the employee’s hobbies in String. Our task involves walking through the employees. If an employee lists “Tennis” as one of their hobbies, we consider them a potential candidate to participate as a tennis player in the tournament. Thus, in the end, we’ll have a list of TennisPlayerCandidate instances:

class TennisPlayerCandidate {
    private final String name;
    private final String email;
    private final Boolean confirmed = Boolean.FALSE;
    public TennisPlayerCandidate(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
  // equals() and hashcode() methods are omitted
}

As our input, let’s assume the EMPLOYEES list contains five objects:

final static List<Employee> EMPLOYEES = Lists.newArrayList(
  new Employee("Kai", "[email protected]", Lists.newArrayList("Football", "Reading", "Chess")),
  new Employee("Eric", "[email protected]", Lists.newArrayList("Tennis", "Baseball", "Singing")),
  new Employee("Saajan", "[email protected]", Lists.newArrayList("Tennis", "Baseball", "Reading")),
  new Employee("Kevin", "[email protected]", Lists.newArrayList("Dancing", "Computer Games", "Tennis")),
  new Employee("Amanda", "[email protected]", Lists.newArrayList("Painting", "Yoga", "Dancing"))
);

Based on this input, we aim to get this list of TennisPlayerCandidate instances:

final static List<TennisPlayerCandidate> EXPECTED = Lists.newArrayList(
  new TennisPlayerCandidate("Eric", "[email protected]"),
  new TennisPlayerCandidate("Saajan", "[email protected]"),
  new TennisPlayerCandidate("Kevin", "[email protected]")
);

Next, let’s see different solutions to build up the expected List<TennisPlayerCandidate> from the given List<Employee>.

For simplicity, we’ll use unit test assertions to verify whether each approach can produce the expected result.

3. Using the List.forEach() Method

One straightforward approach to solve this problem is to start by initializing an empty candidate list. Then, we traverse the EMPLOYEES list, creating a TennisPlayerCandidate object for each employee who lists tennis as a hobby. We add employees to the candidate list if they meet this criterion.

Java 8 introduced the forEach() method, which allows us to perform actions while traversing through a list conveniently:

List<TennisPlayerCandidate> result = new ArrayList<>();
EMPLOYEES.forEach(e -> {
    if (e.getHobbies().contains("Tennis")) {
        result.add(new TennisPlayerCandidate(e.getName(), e.getEmail()));
    }
});
assertEquals(EXPECTED, result);

As we can see, this approach does the job effectively.

Apart from the forEach() method, since Java 8, the Stream API has revolutionized how we manipulate and transform data collections.

Next, let’s use the Stream API to solve the problem.

4. Using Stream.map() or Collectors.mapping()

We can interpret the problem in this way: filtering the employees whose hobbies cover tennis and transforming these Employee objects into TennisPlayerCandidate objects.

Stream’s filter() and map() methods can support us to finish the task easily. Next, let’s “translate” the idea into Java code:

List<TennisPlayerCandidate> result = EMPLOYEES.stream()
  .filter(e -> e.getHobbies().contains("Tennis"))
  .map(e -> new TennisPlayerCandidate(e.getName(), e.getEmail()))
  .collect(Collectors.toList());
assertEquals(EXPECTED, result);

As the code above shows, preparing an empty list for TennisPlayerCandidate objects is unnecessary. The filter().map() pipeline delivers a Stream of TennisPlayerCandidate instances. What we need to do is just collect the objects into a list.

Alternatively, we can move the mapping logic to the collecting phase. In other words, we transform the filtered Employee instances to TennisPlayerCandidate when we collect them. 

The Collectors.mapping() method allows us to perform object transformation and collection from a Stream:

List<TennisPlayerCandidate> result = EMPLOYEES.stream()
  .filter(e -> e.getHobbies().contains("Tennis"))
  .collect(Collectors.mapping(e -> new TennisPlayerCandidate(e.getName(), e.getEmail()), Collectors.toList()));
assertEquals(EXPECTED, result);

5. Conclusion

In this article, we’ve explored three approaches to creating a list of objects of a different type based on a given list. Through the examples, we learned that the Stream API enhances the productivity and readability of code when working with lists in Java.

As always, the complete source code for the examples is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!