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 返回一个恒等函数。
Function.identity
提供了一个按原样返回其参数的函数。characteristics
方法返回一个不可变的收集器特征集,这些特征定义了它的行为。如果集合包含 CONCURRENT
,则可以并行执行收集以进行优化。
Collectors.toList
Collectors.toList
返回一个收集器,它将输入元素累积到一个新列表中。
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
,它按遇到顺序将输入元素连接成一个字符串。
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 中元素的数量。
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
,它生成应用于输入元素的整数值函数的总和。
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 转换。
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
。
在以下示例中,我们创建一个自定义收集器。
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 元素分成组。
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 分成两组。
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教程。