1. 概述

在使用Java编程时,无论是在早期的Java代码中还是从Java 8及以后版本中利用流(Stream API)的优雅功能,对列表中的每个元素调用方法都是基础操作。

本教程将深入探讨针对列表元素调用方法的方法和技术。

2. 问题介绍

让我们通过一个例子快速理解这个问题。假设我们有一个Player类:

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
}

然后,我们初始化一个Player列表作为输入:

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));

现在假设我们想要在PLAYERS列表中的每个玩家执行一个方法。然而,这个需求可能有两种情况:

  • 对每个元素执行操作,不关心返回值,比如打印每个玩家的名字
  • 方法返回结果,实际上将输入列表转换为另一个列表,例如,从PLAYERS提取玩家名字到一个新的List<String>

在这篇教程中,我们将讨论这两种情况,并看看如何在Java 8之前和之后实现它们。

接下来,让我们实际操作一下。

3. 传统方法(Java 8 之前)

在Java 8之前,当我们想在列表中的每个元素上调用方法时,循环是基本的技术。然而,一些外部库可能提供了更方便的方法来高效地解决这个问题。

下面我们深入了解这些方法。

3.1. 对每个元素执行操作

遍历元素并调用方法是针对每个元素执行操作的最直接解决方案

for (Player p : PLAYERS) {
    log.info(p.getName());
}

运行上述的for循环后,在控制台检查输出,我们会看到打印出每个玩家的名字:

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. 转换为另一个列表

类似地,如果我们想通过调用player.getName()提取玩家的名字,我们可以首先声明一个空字符串列表,然后在循环中添加每个玩家的名字

List<String> names = new ArrayList<>();
for (Player p : PLAYERS) {
    names.add(p.getName());
}
assertEquals(Arrays.asList("Kai", "Eric", "Saajan", "Kevin", "John"), names);

3.3. 使用Guava的transform()方法

另一种选择是使用GuavaLists.transform()方法来应用列表变换。

首先,我们需要在pom.xml文件中添加Guava依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

你可以在这里查看Guava的最新版本:https://mvnrepository.com/artifact/com.google.guava/guava

然后,我们可以使用Lists.transform()方法:

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);

如上所示,我们向transform()方法传递了一个匿名的Function<Player, String>对象。当然,我们必须实现Function<F, T>接口的apply()方法,以执行从FPlayer)到TString)的转换逻辑

4. Java 8及更高版本:流(Stream API)

Java 8引入了流(Stream API),为处理集合提供了一种便捷的方式。接下来,我们将看看如何在Java 8及以上版本中解决这个问题。

4.1. 对每个元素执行操作

在Java 8中,Iterable接口的新方法forEach()

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

如图所示,forEach()方法封装了循环实现,使得调用者代码更易于阅读

由于IterableCollection接口的超类型,所以forEach()方法**适用于所有Collection类型,如ListSet**。

forEach()方法期望一个Consumer对象作为参数,非常适合对列表的每个元素执行操作。例如,运行以下代码:

PLAYERS.forEach(player -> log.info(player.getName()));

我们看到预期的输出被打印出来:

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

在上面的代码中,我们向forEach()方法传递了一个lambda表达式作为Consumer对象。

4.2. 转换为另一个列表

要通过应用特定函数并在新列表中收集修改后的元素来转换列表中的元素,可以使用Stream.map()方法。当然,我们首先需要调用stream()将列表转换为Stream,然后使用collect()收集变换后的元素:

List<String> names = PLAYERS.stream()
  .map(Player::getName)
  .collect(Collectors.toList());
assertEquals(List.of("Kai", "Eric", "Saajan", "Kevin", "John"), names);

与Guava的Lists.transform()相比,Stream.map()方法更加流畅,更容易理解。

值得注意的是,我们在map()方法中传递的"Player::getName"是一个方法引用。如果我们将方法引用替换为lambda表达式 "player -> player.getName()",它同样有效。

5. 总结

在这篇文章中,我们探讨了对列表元素调用方法的两种场景。我们详细介绍了各种解决这个问题的方法,考虑了Java 8之前和之后的不同版本。

如往常一样,示例代码的完整源码可在GitHub上找到。