ZetCode

Java forEach

上次修改:2025 年 6 月 17 日

在本文中,我们将演示如何有效地利用 Java 的 forEach 方法。我们将探讨它与 Consumer 的应用,并提供使用 forEach 迭代列表、Map 和 Set 集合的实际示例,展示其在处理各种数据结构方面的多功能性。

forEach 方法对 Iterable 对象中的每个元素执行指定的动作,直到所有元素都处理完毕或发生异常为止。Java 8 中引入的这种方法为开发人员提供了一种现代、简洁的替代传统循环结构的方法,简化了迭代集合的过程。

void forEach(Consumer<? super T> action);

上面的代码片段说明了 forEach 方法的语法,它接受一个 Consumer 对象来定义要对每个元素执行的动作。

Consumer 接口

Consumer 接口是 Java 中的一个函数式接口,其特征在于一个抽象方法。它接受一个输入参数,不返回任何结果,使其非常适合于处理数据而不产生输出的操作,例如打印或修改元素。

@FunctionalInterface
public interface Consumer {
    void accept(T t);
}

此代码定义了 Consumer 接口,突出了其 accept 方法,该方法处理类型为 T 的输入参数。

Main.java
void main() {

    List<String> items = new ArrayList<>();

    items.add("coins");
    items.add("pens");
    items.add("keys");
    items.add("sheets");

    items.forEach(new Consumer<String>() {
        @Override
        public void accept(String name) {
            System.out.println(name);
        }
    });
}

在此示例中,我们将演示如何使用 forEach 方法迭代字符串列表。在这里,我们显式地实现 Consumer 接口来打印每个元素。这种冗长的方法可以使用 Java 的 lambda 表达式进行简化,如下所示。

Lambda 表达式

Java 中的 Lambda 表达式提供了一种简洁的方式来实现函数式接口,例如 Consumer,方法是在内联定义它们的行为。它们使用 -> 运算符构造,该运算符将参数与表达式的主体分开,从而提高代码的可读性并减少样板代码。

Main.java
void main() {

    List<String> items = new ArrayList<>();

    items.add("coins");
    items.add("pens");
    items.add("keys");
    items.add("sheets");

    items.forEach((String name) -> {
        System.out.println(name);
    });
}

此示例重新审视了先前的列表迭代,现在使用 lambda 表达式。通过用 lambda 替换显式的 Consumer 实现,代码变得更简洁易读,同时保持了打印每个字符串的相同功能。

在 Map 上使用 forEach

下一个示例演示了 forEach 方法在 Map(一种存储键值对的集合类型)上的应用。

Main.java
void main() {

    Map<String, Integer> items = new HashMap<>();

    items.put("coins", 3);
    items.put("pens", 2);
    items.put("keys", 1);
    items.put("sheets", 12);

    items.forEach((k, v) -> {
        System.out.printf("%s : %d%n", k, v);
    });
}

在这里,我们定义了一个包含字符串和整数对的 Map。使用带有 lambda 表达式的 forEach 方法,我们迭代 Map,以格式化的方式打印每个键值对。lambda 表达式接受两个参数:键 (k) 和值 (v)。

在下面的示例中,我们显式地定义了 Consumer 并利用 Map.Entry 迭代 Map 的条目,提供了一种替代方法。

Main.java
void main() {

    HashMap<String, Integer> map = new HashMap<>();

    map.put("cups", 6);
    map.put("clocks", 2);
    map.put("pens", 12);

    Consumer<Map.Entry<String, Integer>> action = entry ->
    {
        System.out.printf("key: %s", entry.getKey());
        System.out.printf(" value: %s%n", entry.getValue());
    };

    map.entrySet().forEach(action);
}

此示例通过其 entrySet 方法访问 Map 的条目集来迭代 Map。我们定义了一个 Consumer 来处理每个 Map.Entry 对象,分别打印键和值,从而演示了一种更显式的迭代技术。

Java forEach 在 Set 上

以下示例展示了 forEach 方法在 Set 上的使用,Set 是一种确保其元素之间唯一性的集合。

Main.java
void main() {

    Set<String> brands = new HashSet<>();

    brands.add("Nike");
    brands.add("IBM");
    brands.add("Google");
    brands.add("Apple");

    brands.forEach((e) -> System.out.println(e));
}

在这种情况下,我们创建了一组品牌名称。使用带有 lambda 表达式的 forEach 方法,我们迭代 Set 并打印每个元素,利用这种方法的简单性来显示唯一值。

在数组上使用 forEach

此示例说明了如何将 forEach 方法应用于数组,这需要首先将数组转换为流。

Main.java
void main() {

    int[] nums = {3, 4, 2, 1, 6, 7};

    Arrays.stream(nums).forEach((e) -> System.out.println(e));
}

在这里,我们处理一个整数数组。通过使用 Arrays.stream 方法,我们将数组转换为流,从而可以使用 forEach 迭代并打印每个元素。这种方法弥合了数组和 Java 8 中引入的基于流的操作之间的差距。

过滤列表

forEach 方法可以与过滤操作结合使用,仅处理集合中的特定元素,从而增强其效用。

Main.java
void main() {

    List<String> items = new ArrayList<>();

    items.add("coins");
    items.add("pens");
    items.add("keys");
    items.add("sheets");

    items.stream().filter(item -> item.length() == 4).forEach(System.out::println);
}

在此示例中,我们过滤字符串列表,仅包含那些具有四个字符的字符串,然后使用 forEach 打印结果。应用于列表流的 filter 方法与 forEach 无缝协作,演示如何在处理数据之前对其进行优化。

IntConsumer、LongConsumer、DoubleConsumer

自 Java 8 以来,原始数据类型的专用 Consumer 接口 - IntConsumerLongConsumerDoubleConsumer - 已可用,通过避免自动装箱来优化性能。

Main.java
void main() {

    int[] inums = { 3, 5, 6, 7, 5 };
    IntConsumer icons = i -> System.out.print(i + " ");
    Arrays.stream(inums).forEach(icons);
    
    System.out.println();

    long[] lnums = { 13L, 3L, 6L, 1L, 8L };
    LongConsumer lcons = l -> System.out.print(l + " ");
    Arrays.stream(lnums).forEach(lcons);
    
    System.out.println();

    double[] dnums = { 3.4d, 9d, 6.8d, 10.3d, 2.3d };
    DoubleConsumer dcons = d -> System.out.print(d + " ");
    Arrays.stream(dnums).forEach(dcons);
    
    System.out.println();
}

此示例演示了如何使用 IntConsumerLongConsumerDoubleConsumer 来迭代整数、长整数和双精度数的数组。每个 Consumer 都使用 lambda 表达式定义,以打印带有空格分隔符的元素,展示了它们对原始流的应用。

使用 forEach 链接操作

forEach 方法可以与其他流操作(例如映射和过滤)结合使用,以在处理元素之前执行复杂的转换。这种方法允许在 Java 中进行优雅的函数式编程,使开发人员能够以可读且高效的方式链接操作。

Main.java
void main() {

    List<String> words = new ArrayList<>();

    words.add("apple");
    words.add("banana");
    words.add("cherry");
    words.add("date");

    words.stream()
         .map(String::toUpperCase)
         .filter(word -> word.length() > 5)
         .forEach(System.out::println);
}

在此示例中,我们从水果名称列表开始。使用流,我们链接了三个操作:首先,map 方法使用方法引用 (String::toUpperCase) 将每个字符串转换为大写;接下来,filter 方法仅选择长度超过五个字符的字符串;最后,forEach 方法将结果元素打印到控制台。这表明 forEach 如何充当流管道中的终端操作,有效地处理转换后的数据并展示 Java 的函数式编程功能。

将 forEach 与记录一起使用

forEach 方法具有高度的适应性,可以迭代自定义类型的集合,例如 Java 记录。Java 16 中引入的记录提供了一种简洁的方式来定义不可变的数据类。此示例说明了 forEach 如何处理记录集合,从而简化了结构化数据的处理。

Main.java
record Person(String name, int age) {
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

void main() {

    List<Person> people = new ArrayList<>();

    people.add(new Person("Alice", 25));
    people.add(new Person("Bob", 30));
    people.add(new Person("Clara", 28));

    people.forEach(person -> System.out.println(person));
}

在这种情况下,我们定义了一个具有两个组件的 Person 记录:nameage。记录会自动提供构造函数、getter 和标准方法(如 equalshashCode),但我们覆盖 toString 以自定义输出格式。

然后,我们创建一个 Person 记录列表,并使用带有 lambda 表达式的 forEach 方法迭代该集合,打印每个人的详细信息。此示例重点介绍了 forEach 如何与记录无缝集成,利用其简洁的语法和不变性来有效地处理自定义数据类型。

来源

Java 语言基础 - 教程

在本文中,我们已经彻底探讨了 Java forEach 方法。我们介绍了 Consumer 的概念,并演示了它们在列表、Map 和 Set 中与 forEach 的使用,从而全面理解了这个强大的迭代工具。

作者

我的名字是 Jan Bodnar,我是一位经验丰富的专业程序员。我于 2007 年开始撰写编程文章,此后撰写了超过 1,400 篇文章和八本电子书。凭借超过八年的教学经验,我致力于分享我的知识并帮助他人掌握编程概念。

列出所有Java教程