ZetCode

Java Stream collect

上次修改于 2025 年 5 月 28 日

在本文中,我们将展示如何使用收集器进行归约操作。

Java Stream 是来自源的一系列元素,该源支持聚合操作。Stream 不存储元素;元素是按需计算的。元素从数据源(例如集合、数组或 I/O 资源)中使用。

collect 方法

Java Stream collect 是一个终端 Stream 操作。它对 Stream 的元素执行可变归约操作。归约操作可以按顺序或并行执行。

收集器

Collectors 类包含预定义的收集器,用于执行常见的可变归约任务。收集器将元素累积到集合中,将元素归约为单个值(例如 min、max、count 或 sum),或按条件对元素进行分组。

Set<String> uniqueVals = vals.collect(Collectors.toSet());

toSet 方法返回一个 Collector,它将输入元素累积到一个新的 Set 中。

// can be replaced with min()
Optional<Integer> min = vals.stream().collect(Collectors.minBy(Integer::compareTo));

minBy 返回一个 Collector,它根据给定的 Comparator 生成最小的元素。

Map<Boolean, List<User>> usersByStatus =
    users().stream().collect(Collectors.groupingBy(User::isSingle));

使用 groupingBy,我们将具有 single 状态的用户选择到一个组中。

Map<Boolean, List<User>> statuses =
    users().stream().collect(Collectors.partitioningBy(User::isSingle));

使用 partitioningBy,我们根据用户的 status 属性将用户分成两组。

Collector

Collector 接口定义了一组在归约过程中使用的方法。下面是它的接口签名,它声明了五个基本方法

public interface Collector<T,A,R> {

    Supplier<A> supplier();
    BiConsumer<A,T> accumulator();
    BinaryOperator<A> combiner();
    Function<A,R> finisher();
    Set<Characteristics> characteristics();
}

一个 Collector 由四个函数指定,这些函数协同工作以将条目累积到可变结果容器中,并可选择对结果执行最终转换。

T 表示从 Stream 中收集的元素的类型。A 是累加器的类型——中间结果持有者。R 是收集器返回的最终结果的类型。

supplier 提供了一个创建新结果容器的函数。accumulator 返回一个执行归约操作的函数。它接受两个参数:第一个是可变结果容器(累加器),第二个是正在折叠到其中的 Stream 元素。

对于并行收集,combiner 合并两个累加器。finisher 将中间结果转换为类型为 R 的最终输出。当不需要转换时,finisher 返回一个恒等函数。

注意:在数学中,恒等函数输出其输入不变。在 Java 中,Function.identity 提供了一个按原样返回其参数的函数。

characteristics 方法返回一个不可变的收集器特征集,这些特征定义了它的行为。如果集合包含 CONCURRENT,则可以并行执行收集以进行优化。

Collectors.toList

Collectors.toList 返回一个收集器,它将输入元素累积到一个新列表中。

Main.java
void main() {

    var words = List.of("marble", "coin", "forest", "falcon",
            "sky", "cloud", "eagle", "lion");

    // filter all four character words into a list
    var words4 = words.stream().filter(word -> word.length() == 4)
            .collect(Collectors.toList());

    System.out.println(words4);
}

该示例过滤字符串列表并将 Stream 转换为列表。我们过滤该列表,使其仅包含长度等于 4 的字符串。

var words4 = words.stream().filter(word -> word.length() == 4)
    .collect(Collectors.toList());

通过 stream 方法,我们从字符串列表创建一个 Java Stream。在此 Stream 上,我们应用 filter 方法。filter 方法接受一个匿名函数,对于 Stream 中所有长度为 4 的元素,该函数返回布尔值 true。我们使用 collect 方法从 Stream 中创建一个列表。

$ java Main.java
[coin, lion]

这两个词有四个字符。

Collectors.joining

Collectors.joining 返回一个 Collector,它按遇到顺序将输入元素连接成一个字符串。

Main.java
void main() {

    var words = List.of("marble", "coin", "forest", "falcon",
            "sky", "cloud", "eagle", "lion");

    // can be replaced with String.join
    var joined = words.stream().collect(Collectors.joining(","));

    System.out.printf("Joined string: %s", joined);
}

我们有一个单词列表。我们将该列表转换为一个字符串,其中单词用逗号分隔。

$ java Main.java
Joined string: marble,coin,forest,falcon,sky,cloud,eagle,lion

Collectors.counting

Collectors.counting 返回一个 Collector,它计算 Stream 中元素的数量。

Main.java
void main() {

    var vals = List.of(1, 2, 3, 4, 5);

    // can be replaced with count
    var n = vals.stream().collect(Collectors.counting());

    System.out.println(n);
}

该示例计算列表中元素的数量。

Collectors.summintInt

Collectors.summintInt 返回一个 Collector,它生成应用于输入元素的整数值函数的总和。

Main.java
void main() {

    var cats = List.of(
            new Cat("Bella", 4),
            new Cat("Othello", 2),
            new Cat("Coco", 6)
    );

    // can be replaced with mapToInt().sum()
    var ageSum = cats.stream().collect(Collectors.summingInt(cat -> cat.age()));

    System.out.printf("Sum of cat ages: %d%n", ageSum);
}

record Cat(String name, int age) {
}

该示例计算猫的年龄总和。

var ageSum = cats.stream().collect(Collectors.summingInt(cat -> cat.getAge()));

summingInt 方法的参数是一个 mapper 函数,它提取要相加的属性。

Collectors.collectingAndThen

Collectors.collectingAndThen 调整一个 Collector 以执行额外的 finishing 转换。

Main.java
void main() {

    var vals = List.of(230, 210, 120, 250, 300);

    var avgPrice = vals.stream().collect(Collectors.collectingAndThen(
            Collectors.averagingInt(Integer::intValue),
            avg -> {
                var nf = NumberFormat.getCurrencyInstance(Locale.of("en", "US"));
                return nf.format(avg);
            })
    );

    System.out.printf("The average price is %s%n", avgPrice);
}

该示例计算平均价格,然后对其进行格式化。

$ java Main.java
The average price is $222.00

Collector.of

Collector.of 返回一个由给定的 supplier、accumulator、combiner 和 finisher 函数描述的新 Collector

在以下示例中,我们创建一个自定义收集器。

Main.java
void main() {

    List<User> persons = List.of(
            new User("Robert", 28),
            new User("Peter", 37),
            new User("Lucy", 23),
            new User("David", 28));

    Collector<User, StringJoiner, String> personNameCollector =
            Collector.of(
                    () -> new StringJoiner(" | "), // supplier
            (j, p) -> j.add(p.name()),  // accumulator
            (j1, j2) -> j1.merge(j2),      // combiner
            StringJoiner::toString);       // finisher

    String names = persons
            .stream()
            .collect(personNameCollector);

    System.out.println(names);
}

record User(String name, int age) {}

在该示例中,我们从用户对象列表中收集名称。

Collector<User, StringJoiner, String> personNameCollector =
...

Collector<User, StringJoiner, String> 有三种类型。第一个是新收集器的输入元素的类型。第二个是中间结果的类型,第三个是最终结果的类型。

Collector.of(
        () -> new StringJoiner(" | "), // supplier
        (j, p) -> j.add(p.getName()),  // accumulator
        (j1, j2) -> j1.merge(j2),      // combiner
        StringJoiner::toString);       // finisher

我们创建我们的自定义收集器。首先,我们构建一个初始结果容器。在我们的例子中,它是一个 StringJoiner。累加器只是将当前用户对象的名称添加到 StringJoiner。combiner 在并行处理的情况下合并两个部分结果。最后,finisher 将 StringJoiner 转换为纯字符串。

$ java Main.java
Robert | Peter | Lucy | David

Collectors.groupingBy

使用 Collectors.groupingBy 方法,我们可以根据指定的标准将 Stream 元素分成组。

Main.java
void main() {

    Map<String, List<Product>> productsByCategories =
            products().stream().collect(
                    Collectors.groupingBy(Product::category));

    productsByCategories.forEach((k, v) -> {

        System.out.println(k);

        for (var name : v) {
            System.out.println(name);
        }
    });
}

private List<Product> products() {

    return List.of(
            new Product("apple", "fruit", new BigDecimal("4.50")),
            new Product("banana", "fruit", new BigDecimal("3.76")),
            new Product("carrot", "vegetables", new BigDecimal("2.98")),
            new Product("potato", "vegetables", new BigDecimal("0.92")),
            new Product("garlic", "vegetables", new BigDecimal("1.32")),
            new Product("ginger", "vegetables", new BigDecimal("2.45")),
            new Product("white bread", "bakery", new BigDecimal("1.50")),
            new Product("roll", "bakery", new BigDecimal("0.08")),
            new Product("bagel", "bakery", new BigDecimal("0.15"))
    );
}

record Product(String name, String category, BigDecimal price) {
}

我们有一个产品列表。使用 Collectors.groupingBy,我们根据产品的类别将产品分成组。

$ java Main.java
bakery
Product{name='white bread', category='bakery', price=1.50}
Product{name='roll', category='bakery', price=0.08}
Product{name='bagel', category='bakery', price=0.15}
fruit
Product{name='apple', category='fruit', price=4.50}
Product{name='banana', category='fruit', price=3.76}
vegetables
Product{name='carrot', category='vegetables', price=2.98}
Product{name='potato', category='vegetables', price=0.92}
Product{name='garlic', category='vegetables', price=1.32}
Product{name='ginger', category='vegetables', price=2.45}

Collectors.partitioningBy

分区是分组的一种特殊情况。分区操作根据给定的谓词函数将 Stream 分成两组。

Main.java
void main() {

    Map<Boolean, List<User>> statuses =
            users().stream().collect(
                    Collectors.partitioningBy(User::single));

    statuses.forEach((k, v) -> {

        if (k) {

            System.out.println("Single: ");
        } else {

            System.out.println("In a relationship:");
        }

        v.forEach(System.out::println);
    });
}

private List<User> users() {

    return List.of(
            new User("Julia", false),
            new User("Jake", false),
            new User("Mike", false),
            new User("Robert", true),
            new User("Maria", false),
            new User("Peter", true)
    );
}

record User(String name, boolean single) {
}

在该示例中,我们根据 single 属性将 Stream 分成两组。

Map<Boolean, List<User>> statuses =
    users().stream().collect(Collectors.partitioningBy(User::single));

Collectors.partitioningBy 采用 single 谓词,该谓词返回一个布尔值,指示用户的状态。

$ java Main.java
In a relationship:
User{name='Julia', single=false}
User{name='Jake', single=false}
User{name='Mike', single=false}
User{name='Maria', single=false}
Single: 
User{name='Robert', single=true}
User{name='Peter', single=true}

来源

Java Stream 文档

在本文中,我们使用了 Java Stream 预定义和自定义收集器。

作者

我叫 Jan Bodnar,我是一位充满热情的程序员,拥有丰富的编程经验。自 2007 年以来,我一直在撰写编程文章。到目前为止,我已经撰写了超过 1,400 篇文章和 8 本电子书。我拥有超过十年的编程教学经验。

列出所有Java教程