网站首页 > 博客文章 正文
Collector是一种特殊类型的终端操作。Collector触发流的计算。它将来自连接到源的 Stream 的数据收集到指定的 Collection 对象中。
让我们看下面的字符串流。
Stream<String> nameStream = Stream.of("NAME1", "NAME2", "NAME3",
"NAME4", "NAME5", "NAME6",
"NAME7", "NAME8", "NAME9",
"", "", " ");
我们希望将所有非空字符串添加到 List 集合中。我们可以通过先修剪字符串,然后过滤修剪后的空字符串(如果有)来实现此目的,如下所示。
List<String> names = new ArrayList<>();
nameStream
.map(s -> s.trim())
.filter(s -> !s.isEmpty())
.forEach(s -> names.add(s));
或者我们可以使用下面的方法参考来更具声明性。
nameStream
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.forEach(names::add);
我们已经在本系列文章中多次看到过使用 forEach 和 println 的此类示例。在这里,我们不是打印,而是将名称添加到 List 对象中。但这种方法有一个重大缺点。
请记住,无论我们在 lambda 或方法引用中使用什么数据,都应该始终是有效的最终数据。如果您观察到,我们正在将元素添加到外部列表中。这意味着我们在 forEach 中使用的 lambda 正在改变 lambda 外部的集合的状态。如果我们以并发方式使用它,这会产生不一致的结果,因为 ArrayList 不是并发集合。这意味着我们不能将此逻辑与并行流一起使用。
这就是Collector介入并帮助我们的地方。
专业提示:永远不要使用外部集合从流的终端操作中添加元素。
“Collectors”和“Collect”类
Stream中的collect()方法是一个重载方法。还引入了一个名为 Collectors 的帮助器类,它包含一堆静态工厂方法,这些方法返回预定义的 Collector 对象以将元素收集到相应的集合中。
1. Collectors.toList()
我们可以使用 Collectors.toList() 和collect()方法来代替常规的ArrayList,如下所示。
List<String> names = nameStream
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.toList());
静态工厂方法 Collectors.toList() 返回一个 Collector 对象,该对象将元素收集到 List 对象中。现在,collect() 方法是一个终端操作,它只需获取此 Collector 对象并神奇地返回一个不可变的 List 对象。
2. Collectors.toSet()
我们甚至可以将名称收集到一个集合中。我们只需要使用 Collectors.toSet() 如下。
Set<String> nameSet = nameStream
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.toSet());
toList() 默认情况下使用 ArrayList,而 toSet() 底层使用 HashSet。但是如果想给出我们自己的 List 或 Set 集合怎么办?这就是 Collectors.toCollection() 派上用场的地方
3. Collectors.toMap()
我们还可以将 Stream 的元素收集到 Map 中。假设我们想要将上述 name Stream 的字符串收集到一个map中,该map字符串为键,字符串的长度为值。那么我们可以按照下面的方法来做。
Map<String, Integer> map = nameStream.
map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.toMap(s -> s, String::length));
toMap() 方法有两个参数。两者都是 Function 类型。第一个参数用于键,第二个参数用于map中的值。看一下第一个参数 s -> s。这个 lambda 没有做任何事情,输入一个字符串并返回相同的字符串。
出于这种目的,Function 类中存在一个名为 Identity() 的方法,它可以执行相同的操作。建议使用此方法而不是使用 lambda。这样写
Map<String, Integer> map = nameStream.
map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.toMap(Function.identity(),
String::length));
4. Collectors.toCollection()
我们甚至可以使用 Collectors.toCollection() 将所有元素收集到我们自己的集合中,如下所示。
收集到 LinkedList 而不是 ArrayList
List<String> names = nameStream
.map(s -> s.trim())
.filter(s -> !s.isEmpty())
.collect(Collectors.toCollection(LinkedList::new));
收集到 TreeSet 而不是 HashSet
Set<String> nameSet = nameStream
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.toCollection(TreeSet::new));
5. Collectors.joining()
当我们想要使用如下分隔符连接字符串格式的所有元素时, join() 会派上用场。
String s = Stream.of("one", "two", "three").
map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.joining(", "));
System.out.println(s);
流中的所有字符串都通过逗号作为分隔符连接。
6. Collectors.partitioningBy()
paritioningBy() 方法有助于根据特定条件拆分流的元素。
我们想要的是一个 Employee 员工对象列表,以这样的方式进行分区:第一个分区包含工资大于 1000 的员工。第二个分区包含工资小于或等于 1000 的员工。可以使用 Collectors.partitioningBy() 来实现,如下所示
List<Employee> employees = List.of(
new Employee(1, "EMP1", Employee.Sex.FEMALE, 900.0),
new Employee(2, "EMP2", Employee.Sex.MALE, 1290.0),
new Employee(3, "EMP3", Employee.Sex.FEMALE, 1200.0),
new Employee(4, "EMP4", Employee.Sex.MALE, 900.0),
new Employee(5, "EMP5", Employee.Sex.FEMALE, 1290.0)
);
以下是我们如何使用 Collectors.paritioningBy()。
Map<Boolean, List<Employee>> greater1000Partition =
employees.stream().collect(
Collectors.partitioningBy(emp -> emp.getSalary() > 1000));
PartitioningBy() 方法采用Predicate(不懂可以看我函数式编程系列文章)来对元素进行分区。
现在,返回类型:Map<Boolean, List<Employee>>。更有趣的是。返回值表示映射中的键是布尔类型。该map:
键为 true 且值为与给定Predicate匹配的 Employee 对象列表的一项。
键为 false 且值 Employee 对象列表与给定Predicate不匹配的另一个条目。
使用下面的代码打印map的值
greater1000Partition.forEach((k, v) -> {
String value = v.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(k + " === " + value);
}
);
上面的代码打印了布尔值键和与Predicate匹配的每个员工的姓名,然后使用 Collectors.joining() 通过逗号连接所有姓名。这是输出。
false === EMP1, EMP4
true === EMP2, EMP3, EMP5
7. Collectors.groupingBy()
假设我们想要对 Employee 对象进行分组或分类,以便相同的薪资员工属于一组,那么我们可以使用 groupingBy() 方法。
groupingBy() 收集器与partitioningBy() 类似。唯一的不同是partitioningBy() 接受一个Predidate,而groupingBy() 接受一个Function。该函数充当分类器,根据该分类器进行分组。
Map<Double, List<Employee>> empGroupBySalary =
employees.stream()
.collect(Collectors.groupingBy(Employee::getSalary));
现在可以看到返回类型的变化。键的类型为 Double,代表工资。谁拿同样的薪水就属于这个组。
empGroupBySalary.forEach((k, v) -> {
String value = v.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(k + " === " + value);
}
);
输出
1200.0 === EMP3
1290.0 === EMP2, EMP5
900.0 === EMP1, EMP4
8. Collectors.mapping()
在上面的示例中,我们得到的是 Map<Double, List<Employee>>。但是,如果我们想要一个 Map<Double, List<String>> 只表示员工姓名而不是员工对象本身,该怎么办?
正是出于这些目的,Collector.groupingBy() 有另一种变种。
Collector.groupingBy() 是一个重载方法,并且有另一个带有两个参数的变体:一个函数和一个收集器。第二个收集器称为下游收集器。
这里我们使用 Collectors.mapping() 作为下游收集器,它将 List<Employee> 对象转换为 List<String>。 Collectors.mapping() 采用两个参数,如下面的代码所示。
Map<Double, List<String>> empNamesGroupBySalary =
employees.stream().collect(
Collectors.groupingBy(
Employee::getSalary,
Collectors.mapping(Employee::getName,
Collectors.toList()
)
)
);
第一个参数是表示转换的函数。
第二个参数是收集器,转换后的对象被收集到其中。这里我们使用了列表收集器。
在这里我们可以做一件更有趣的事情。如果我们想要一个包含所有由逗号分隔的名称的字符串,而不是包含所有名称,就像我们在上面的输出中打印的那样,该怎么办?
这很简单。我们使用 join() 收集器代替 toList() 收集器。这样做
Map<Double, String> empNamesGroupBySalary1 =
employees.stream().collect(
Collectors.groupingBy(
Employee::getSalary,
Collectors.mapping(
Employee::getName,
Collectors.joining(", ")
)
)
);
如果我们打印上面的map,
empNamesGroupBySalary1.forEach((k, v) ->
System.out.println(k + " === " + v));
输出
1200.0 === EMP3
1290.0 === EMP2,EMP5
900.0 === EMP1,EMP4
9. Collectors.counting()
如果你正确理解了上面的例子,这非常简单。我们想要的是统计属于每个组的员工数量。我们只需要使用 Collectors.counting() 而不是 Collector.mapping()。
Collectors.couting() 方法不带任何参数。它只是计算元素。
Map<Double, Long> empGroupBySalaryCount =
employees.stream().collect(
Collectors.groupingBy(
Employee::getSalary,
Collectors.counting()));
输出
1200.0 === 1
1290.0 === 2
900.0 === 2
现在知道Collector优雅?想象一下,如果没有Collector,为所有这些场景编写代码是多么痛苦。 groupingBy 收集器是面试中经常被问到的最重要的事情。
10. Collectors.collectingAndThen()
我们所见过的所有上述双参数收集器,例如 groupingBy() 和 mapping() 首先将函数作为第一个参数,将收集器作为第二个参数。他们首先应用函数表示的转换,然后将其收集到指定的收集器中。
但如果我们想要相反的情况呢?我们希望首先收集元素,然后应用转换。 CollectionAndThen() 方法完全具有相同的目的。
假设在上面的 count() 示例中我们得到了一个 Map<Double,Long>。但我们希望计数是一个整数值。默认情况下,counting() 方法返回一个 long 值。在这种情况下,我们可以在应用收集器后使用collectingAndThen()方法将Long转换为Integer。我们是这样做的。
Map<Double, Integer> empGroupBySalaryCount =
employees.stream().collect(
Collectors.groupingBy(
Employee::getSalary,
Collectors.collectingAndThen(
Collectors.counting(),
Long::intValue
)
)
);
这就是 Stream Collector的全部内容。还有其他Collector。但这 10 个是最常用的,涵盖了行业中使用的 90%。
猜你喜欢
- 2024-12-26 Java 8 Stream 处理大数据集:实战与优化
- 2024-12-26 面试官:Java8 lambda 表达式 forEach 如何提前终止?
- 2024-12-26 Javascript中,forEach和map到底有什么区别?
- 2024-12-26 Excel VBA之For Each遍历循环的应用
- 2024-12-26 为什么建议使用 for…of 循环而不是 foreach 循环呢
- 2024-12-26 前端开发map和foreach区别,map遍历方式用法介绍
- 2024-12-26 Rust语言从入门到精通系列 - 零基础掌握Stream流迭代器
- 2024-12-26 Map遍历的四种方法效率对比
- 2024-12-26 java集合类之java中集合类有哪些?如何分类?
- 2024-12-26 【一分钟学Java】之List
你 发表评论:
欢迎- 367℃用AI Agent治理微服务的复杂性问题|QCon
- 358℃初次使用IntelliJ IDEA新建Maven项目
- 357℃手把手教程「JavaWeb」优雅的SpringMvc+Mybatis整合之路
- 351℃Maven技术方案最全手册(mavena)
- 348℃安利Touch Bar 专属应用,让闲置的Touch Bar活跃起来!
- 346℃InfoQ 2024 年趋势报告:架构篇(infoq+2024+年趋势报告:架构篇分析)
- 345℃IntelliJ IDEA 2018版本和2022版本创建 Maven 项目对比
- 342℃从头搭建 IntelliJ IDEA 环境(intellij idea建包)
- 最近发表
- 标签列表
-
- powershellfor (55)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)