1. Overview
When we work with Java, whether we’re working with pre-Java 8 code or embracing the functional elegance of the Stream API in Java 8 and beyond, calling a method on each element of a list is a fundamental operation.
In this tutorial, we’ll explore the methods and techniques available for calling a method on each list element.
2. Introduction to the Problem
As usual, let’s understand the problem quickly through an example. Let’s say we have the Player class:
class Player {
private int id;
private String name;
private int score;
public Player(int id, String name, int score) {
this.id = id;
this.name = name;
this.score = score;
}
// getter and setter methods are omitted
}
Then, let’s initialize a list of Players as our input:
List<Player> PLAYERS = List.of(
new Player(1, "Kai", 42),
new Player(2, "Eric", 43),
new Player(3, "Saajan", 64),
new Player(4, "Kevin", 30),
new Player(5, "John", 5));
Let’s say we want to execute a method on each player in the PLAYERS list. However, this requirement can have two scenarios:
- Perform an action on each element and don’t care about the returned value, such as printing each player’s name
- The method returns a result, effectively transforming the input list to another list, for example, extracting player names into a new List<String> from PLAYERS
In this tutorial, we’ll discuss both scenarios. Furthermore, we’ll see how to achieve them in pre-Java 8 and Java 8+.
Next, let’s see them in action.
3. Traditional Approach (Prior to Java 8)
Before Java 8, loop was the base technique when we wanted to call a method on each element in a list. However, some external libraries may provide convenient methods that allow us to solve the problem more efficiently.
Next, let’s take a close look at them.
3.1. Performing an Action on Each Element
Looping through the elements and calling the method can be the most straightforward solution to perform an action on each element:
for (Player p : PLAYERS) {
log.info(p.getName());
}
If we check the console after running the for loop above, we’ll see the log output. Each player’s name was printed:
21:14:47.219 [main] INFO ... - Kai
21:14:47.220 [main] INFO ... - Eric
21:14:47.220 [main] INFO ... - Saajan
21:14:47.220 [main] INFO ... - Kevin
21:14:47.220 [main] INFO ... - John
3.2. Transforming to Another List
Similarly, if we want to extract players’ names by calling player.getName(), we can first declare an empty string list and add each player’s name in a loop:
List<String> names = new ArrayList<>();
for (Player p : PLAYERS) {
names.add(p.getName());
}
assertEquals(Arrays.asList("Kai", "Eric", "Saajan", "Kevin", "John"), names);
3.3. Using Guava’s transform() Method
Alternatively, we can use Guava‘s Lists.transform() method to apply the list transformation.
The first step to use the Guava library is to add the dependency to our pom.xml:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
The latest Guava version can be checked here.
Then, we can use the Lists.transform() method:
List<String> names = Lists.transform(PLAYERS, new Function<Player, String>() {
@Override
public String apply(Player input) {
return input.getName();
}
});
assertEquals(Arrays.asList("Kai", "Eric", "Saajan", "Kevin", "John"), names);
As the code above shows, we passed an anonymous Function<Player, String> object to the transform() method. Of course, we must implement the apply() method in the Function<F, T> interface to perform the transformation logic from F (Player) to T (String).
4. Java 8 and Beyond: The Stream API
The Stream API was introduced in Java 8, providing a convenient way to work with collections. Next, let’s see how our problem can be solved in Java 8+.
4.1. Performing an Action on Each Element
In Java 8, the forEach() method is a new method in the Iterable interface:
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
As we can see, forEach() wraps the loop implementation and makes the code on the caller’s side easier to read.
As Iterable is the supertype of the Collection interface, forEach() is available in all Collection types, such as List and Set.
The forEach() method expects a Consumer object as the argument. It’s ideal to perform an action on each list’s element. For example, let’s run this line of code:
PLAYERS.forEach(player -> log.info(player.getName()));
We see the expected output get printed:
21:14:47.223 [main] INFO ... - Kai
21:14:47.223 [main] INFO ... - Eric
21:14:47.224 [main] INFO ... - Saajan
21:14:47.224 [main] INFO ... - Kevin
21:14:47.224 [main] INFO ... - John
In the code above, we passed a lambda expression as the Consumer object to the forEach() method.
4.2. Transforming to Another List
To transform elements within a list by applying a specific function and gathering these modified elements into a new list, the Stream.map() method can be employed. Of course, we must first call stream() to convert our list to a Stream, and collect() the transformed elements:
List<String> names = PLAYERS.stream()
.map(Player::getName)
.collect(Collectors.toList());
assertEquals(List.of("Kai", "Eric", "Saajan", "Kevin", "John"), names);
As we can see, compared to Guava’s Lists.transform(), the Stream.map() approach is more fluent and easier to understand.
It’s worth noting that the “Player::getName” we passed to the map() method is a method reference. It works just as well if we replace the method reference with this lambda expression: “player -> player.getName()“.
5. Conclusion
In this article, we explored two scenarios for invoking a method on each element of a list. We delved into various solutions addressing this challenge, considering both pre-Java 8 and Java 8 and later versions.
As always, the complete source code for the examples is available over on GitHub.