本系列文章翻译自@shekhargulatijava8-the-missing-tutorial

你已经学习了Stream API能够让你以声明式的方式帮助你处理集合。我们看到collect是一个将管道流的结果集到一个list中的结束操作。collect是一个将数据流缩减为一个值的归约操作。这个值可以是集合、映射,或者一个值对象。你可以使用collect达到以下目的:

  • 将数据流缩减为一个单一值:一个流执行后的结果能够被缩减为一个单一的值。单一的值可以是一个Collection,或者像int、double等的数值,再或者是一个用户自定义的值对象。

  • 将一个数据流中的元素进行分组:根据任务类型将流中所有的任务进行分组。这将产生一个Map<TaskType, List>的结果,其中每个实体包含一个任务类型以及与它相关的任务。你也可以使用除了列表以外的任何其他的集合。如果你不需要与一任务类型相关的所有的任务,你可以选择产生一个Map<TaskType, Task>。这是一个能够根据任务类型对任务进行分类并获取每类任务中第一个任务的例子。

  • 分割一个流中的元素:你可以将一个流分割为两组——比如将任务分割为要做和已经做完的任务。

Collector实际应用

为了感受到Collector的威力,让我们来看一下我们要根据任务类型来对任务进行分类的例子。在Java8中,我们可以通过编写如下的代码达到将任务根据类型分组的目的。

  1. private static Map<TaskType, List<Task>> groupTasksByType(List<Task> tasks) {
  2. return tasks.stream().collect(Collectors.groupingBy(task -> task.getType()));
  3. }

上面的代码使用了定义在辅助类Collectors中的groupingBy收集器。它创建了一个映射,其中TaskType是它的键,而包含了所有拥有相同TaskType的任务的列表是它的值。为了在Java7中达到相同的效果,你需要编写如下的代码。

  1. public static void main(String[] args) {
  2. List<Task> tasks = getTasks();
  3. Map<TaskType, List<Task>> allTasksByType = new HashMap<>();
  4. for (Task task : tasks) {
  5. List<Task> existingTasksByType = allTasksByType.get(task.getType());
  6. if (existingTasksByType == null) {
  7. List<Task> tasksByType = new ArrayList<>();
  8. tasksByType.add(task);
  9. allTasksByType.put(task.getType(), tasksByType);
  10. } else {
  11. existingTasksByType.add(task);
  12. }
  13. }
  14. for (Map.Entry<TaskType, List<Task>> entry : allTasksByType.entrySet()) {
  15. System.out.println(String.format("%s =>> %s", entry.getKey(), entry.getValue()));
  16. }
  17. }

收集器:常用的规约操作

Collectors辅助类提供了大量的静态辅助方法来创建收集器为常见的使用场景服务,像将元素收集到一个集合中、分组和分割元素,或者根据不同的标准来概述元素。我们将在这篇博文中涵盖大部分常见的Collector。

缩减为一个值

正如上面讨论的,收集器可以被用来收集流的输出到一个集合,或者产生一个单一的值。

将数据收集进一个列表

让我们编写我们的第一个测试用例——给定一个任务列表,我们想将他们的标题收集进一个列表。

  1. import static java.util.stream.Collectors.toList;
  2. public class Example2_ReduceValue {
  3. public List<String> allTitles(List<Task> tasks) {
  4. return tasks.stream().map(Task::getTitle).collect(toList());
  5. }
  6. }

toList收集器使用了列表的add方法来向结果列表中添加元素。toList收集器使用了ArrayList作为列表的实现。

将数据收集进一个集合

如果我们想要确保返回的标题都是唯一的,并且我们不在乎元素的顺序,那么我们可以使用toSet收集器。

  1. import static java.util.stream.Collectors.toSet;
  2. public Set<String> uniqueTitles(List<Task> tasks) {
  3. return tasks.stream().map(Task::getTitle).collect(toSet());
  4. }

toSet方法使用了HashSet作为集合的实现来存储结果集。

将数据收集进一个映射

你可以使用toMap收集器将一个流转换为一个映射。toMap收集器需要两个映射方法来获得映射的键和值。在下面展示的代码中,Task::getTitle是接收一个任务并产生一个只包含该任务标题的键的Function。task -> task是一个用来返回任务本身的lambda表达式。

  1. private static Map<String, Task> taskMap(List<Task> tasks) {
  2. return tasks.stream().collect(toMap(Task::getTitle, task -> task));
  3. }

我们可以通过使用Function接口中的默认方法identity来改进上面展示的代码,如下所示,这样可以让代码更加简洁,并更好地传达开发者的意图。

  1. import static java.util.function.Function.identity;
  2. private static Map<String, Task> taskMap(List<Task> tasks) {
  3. return tasks.stream().collect(toMap(Task::getTitle, identity()));
  4. }

从一个流中创建映射的代码会在存在重复的键时抛出异常。你将会得到一个类似下面的错误。

  1. Exception in thread "main" java.lang.IllegalStateException: Duplicate key Task{title='Read Version Control with Git book', type=READING}
  2. at java.util.stream.Collectors.lambda$throwingMerger$105(Collectors.java:133)

你可以通过使用toMap方法的另一个变体来处理重复问题,它允许我们指定一个合并方法。这个合并方法允许用户他们指定想如何处理多个值关联到同一个键的冲突。在下面展示的代码中,我们只是使用了新的值,当然你也可以编写一个智能的算法来处理冲突。

  1. private static Map<String, Task> taskMap_duplicates(List<Task> tasks) {
  2. return tasks.stream().collect(toMap(Task::getTitle, identity(), (t1, t2) -> t2));
  3. }

你可以通过使用toMap方法的第三个变体来指定其他的映射实现。这需要你指定将用来存储结果的Map和Supplier。

  1. public Map<String, Task> collectToMap(List<Task> tasks) {
  2. return tasks.stream().collect(toMap(Task::getTitle, identity(), (t1, t2) -> t2, LinkedHashMap::new));
  3. }

类似于toMap收集器,也有toConcurrentMap收集器,它产生一个ConcurrentMap而不是HashMap。

使用其它的收集器

像toList和toSet这类特定的收集器不允许你指定内部的列表或者集合实现。当你想要将结果收集到其它类型的集合中时,你可以像下面这样使用toCollection收集器。

  1. private static LinkedHashSet<Task> collectToLinkedHaskSet(List<Task> tasks) {
  2. return tasks.stream().collect(toCollection(LinkedHashSet::new));
  3. }

找到拥有最长标题的任务

  1. public Task taskWithLongestTitle(List<Task> tasks) {
  2. return tasks.stream().collect(collectingAndThen(maxBy((t1, t2) -> t1.getTitle().length() - t2.getTitle().length()), Optional::get));
  3. }

统计标签的总数

  1. public int totalTagCount(List<Task> tasks) {
  2. return tasks.stream().collect(summingInt(task -> task.getTags().size()));
  3. }

生成任务标题的概述

  1. public String titleSummary(List<Task> tasks) {
  2. return tasks.stream().map(Task::getTitle).collect(joining(";"));
  3. }

分类收集器

收集器最常见的使用场景之一是对元素进行分类。让我来看一下不同的例子来理解我们如何进行分类。

例子1:根据类型对任务分类

我们看一下下面展示的例子,我们想要根据TaskType来对所有的任务进行分类。我们可以通过使用Collectors辅助类中的groupingBy方法来轻易地进行该项任务。你可以通过使用方法引用和静态导入来使它更加高效。

  1. import static java.util.stream.Collectors.groupingBy;
  2. private static Map<TaskType, List<Task>> groupTasksByType(List<Task> tasks) {
  3. return tasks.stream().collect(groupingBy(Task::getType));
  4. }

它将会产生如下的输出。

{CODING=[Task{title='Write a mobile application to store my tasks', type=CODING, createdOn=2015-07-03}], WRITING=[Task{title='Write a blog on Java 8 Streams', type=WRITING, createdOn=2015-07-04}], READING=[Task{title='Read Version Control with Git book', type=READING, createdOn=2015-07-01}, Task{title='Read Java 8 Lambdas book', type=READING, createdOn=2015-07-02}, Task{title='Read Domain Driven Design book', type=READING, createdOn=2015-07-05}]}

例子2:根据标签分类

  1. private static Map<String, List<Task>> groupingByTag(List<Task> tasks) {
  2. return tasks.stream().
  3. flatMap(task -> task.getTags().stream().map(tag -> new TaskTag(tag, task))).
  4. collect(groupingBy(TaskTag::getTag, mapping(TaskTag::getTask,toList())));
  5. }
  6. private static class TaskTag {
  7. final String tag;
  8. final Task task;
  9. public TaskTag(String tag, Task task) {
  10. this.tag = tag;
  11. this.task = task;
  12. }
  13. public String getTag() {
  14. return tag;
  15. }
  16. public Task getTask() {
  17. return task;
  18. }
  19. }

例子3:根据标签和数量对任务分类

将分类器和收集器结合起来。

  1. private static Map<String, Long> tagsAndCount(List<Task> tasks) {
  2. return tasks.stream().
  3. flatMap(task -> task.getTags().stream().map(tag -> new TaskTag(tag, task))).
  4. collect(groupingBy(TaskTag::getTag, counting()));
  5. }

例子4:根据任务类型和创建日期分类

  1. private static Map<TaskType, Map<LocalDate, List<Task>>> groupTasksByTypeAndCreationDate(List<Task> tasks) {
  2. return tasks.stream().collect(groupingBy(Task::getType, groupingBy(Task::getCreatedOn)));
  3. }

分割

很多时候你想根据一个断言来将一个数据集分割成两个数据集。举例来说,我们可以通过定义一个将任务分割为两组的分割方法来将任务分割成两组,一组是在今天之前已经到期的,另一组是其他的任务。

  1. private static Map<Boolean, List<Task>> partitionOldAndFutureTasks(List<Task> tasks) {
  2. return tasks.stream().collect(partitioningBy(task -> task.getDueOn().isAfter(LocalDate.now())));
  3. }

生成统计信息

另一组非常有用的收集器是用来产生统计信息的收集器。这能够在像int、double和long这样的原始数据类型上起到作用;并且能被用来生成像下面这样的统计信息。

  1. IntSummaryStatistics summaryStatistics = tasks.stream().map(Task::getTitle).collect(summarizingInt(String::length));
  2. System.out.println(summaryStatistics.getAverage()); //32.4
  3. System.out.println(summaryStatistics.getCount()); //5
  4. System.out.println(summaryStatistics.getMax()); //44
  5. System.out.println(summaryStatistics.getMin()); //24
  6. System.out.println(summaryStatistics.getSum()); //162

也有其它的变种形式,像针对其它原生类型的LongSummaryStatistics和DoubleSummaryStatistics。

你也可以通过使用combine操作来将一个IntSummaryStatistics与另一个组合起来。

  1. firstSummaryStatistics.combine(secondSummaryStatistics);
  2. System.out.println(firstSummaryStatistics)

连接所有的标题

  1. private static String allTitles(List<Task> tasks) {
  2. return tasks.stream().map(Task::getTitle).collect(joining(", "));
  3. }

编写一个定制的收集器

  1. import com.google.common.collect.HashMultiset;
  2. import com.google.common.collect.Multiset;
  3. import java.util.Collections;
  4. import java.util.EnumSet;
  5. import java.util.Set;
  6. import java.util.function.BiConsumer;
  7. import java.util.function.BinaryOperator;
  8. import java.util.function.Function;
  9. import java.util.function.Supplier;
  10. import java.util.stream.Collector;
  11. public class MultisetCollector<T> implements Collector<T, Multiset<T>, Multiset<T>> {
  12. @Override
  13. public Supplier<Multiset<T>> supplier() {
  14. return HashMultiset::create;
  15. }
  16. @Override
  17. public BiConsumer<Multiset<T>, T> accumulator() {
  18. return (set, e) -> set.add(e, 1);
  19. }
  20. @Override
  21. public BinaryOperator<Multiset<T>> combiner() {
  22. return (set1, set2) -> {
  23. set1.addAll(set2);
  24. return set1;
  25. };
  26. }
  27. @Override
  28. public Function<Multiset<T>, Multiset<T>> finisher() {
  29. return Function.identity();
  30. }
  31. @Override
  32. public Set<Characteristics> characteristics() {
  33. return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
  34. }
  35. }
  36. import com.google.common.collect.Multiset;
  37. import java.util.Arrays;
  38. import java.util.List;
  39. public class MultisetCollectorExample {
  40. public static void main(String[] args) {
  41. List<String> names = Arrays.asList("shekhar", "rahul", "shekhar");
  42. Multiset<String> set = names.stream().collect(new MultisetCollector<>());
  43. set.forEach(str -> System.out.println(str + ":" + set.count(str)));
  44. }
  45. }

Java8中的字数统计

我们将通过使用流和收集器在Java8中编写有名的字数统计样例来结束这一节。

  1. public static void wordCount(Path path) throws IOException {
  2. Map<String, Long> wordCount = Files.lines(path)
  3. .parallel()
  4. .flatMap(line -> Arrays.stream(line.trim().split("\\s")))
  5. .map(word -> word.replaceAll("[^a-zA-Z]", "").toLowerCase().trim())
  6. .filter(word -> word.length() > 0)
  7. .map(word -> new SimpleEntry<>(word, 1))
  8. .collect(groupingBy(SimpleEntry::getKey, counting()));
  9. wordCount.forEach((k, v) -> System.out.println(String.format("%s ==>> %d", k, v)));
  10. }

Java8学习笔记(七)--Collectors的更多相关文章

  1. Java8学习笔记目录

    Java8学习笔记(一)--Lambda表达式 Java8学习笔记(二)--三个预定义函数接口 Java8学习笔记(三)--方法引入 Java8学习笔记(四)--接口增强 Java8学习笔记(五)-- ...

  2. Java8学习笔记----Lambda表达式 (转)

    Java8学习笔记----Lambda表达式 天锦 2014-03-24 16:43:30 发表于:ATA之家       本文主要记录自己学习Java8的历程,方便大家一起探讨和自己的备忘.因为本人 ...

  3. (转)Qt Model/View 学习笔记 (七)——Delegate类

    Qt Model/View 学习笔记 (七) Delegate  类 概念 与MVC模式不同,model/view结构没有用于与用户交互的完全独立的组件.一般来讲, view负责把数据展示 给用户,也 ...

  4. Learning ROS for Robotics Programming Second Edition学习笔记(七) indigo PCL xtion pro live

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS forRobotics Pro ...

  5. Typescript 学习笔记七:泛型

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  6. Java8学习笔记(八)--方法引入的补充

    在Java8学习笔记(三)--方法引入中,简要总结了方法引入时的使用规则,但不够完善.这里补充下几种情况: 从形参到实例方法的实参 示例 public class Example { static L ...

  7. python3.4学习笔记(七) 学习网站博客推荐

    python3.4学习笔记(七) 学习网站博客推荐 深入 Python 3http://sebug.net/paper/books/dive-into-python3/<深入 Python 3& ...

  8. Go语言学习笔记七: 函数

    Go语言学习笔记七: 函数 Go语言有函数还有方法,神奇不.这有点像python了. 函数定义 func function_name( [parameter list] ) [return_types ...

  9. iOS 学习笔记七 【博爱手把手教你使用2016年gitHub Mac客户端】

    iOS 学习笔记七 [博爱手把手教你使用gitHub客户端] 第一步:首先下载git客户端 链接:https://desktop.github.com 第二步:fork 大神的代码[这里以我的代码为例 ...

  10. 【opencv学习笔记七】访问图像中的像素与图像亮度对比度调整

    今天我们来看一下如何访问图像的像素,以及如何改变图像的亮度与对比度. 在之前我们先来看一下图像矩阵数据的排列方式.我们以一个简单的矩阵来说明: 对单通道图像排列如下: 对于双通道图像排列如下: 那么对 ...

随机推荐

  1. Python3绘图库Matplotlib(01)

    1 First plots with Matplotlib 简单的绘图1 简单的绘图2 简单的绘图3 2 网格 = grid 3 设置坐标轴的取值范围 = axis xlim ylim 方法1:整体设 ...

  2. APM的3DR无线数传的安装和调试

    APM飞控修改数传模块方法 http://www.cnblogs.com/wsine/p/4909903.html APM的3DR无线数传的安装和调试 http://tieba.baidu.com/p ...

  3. java中动态给sql追加?号

    /* * 用来生成where子句 len数组的长度 */ private String toWhereSql(int len) { StringBuilder sb = new StringBuild ...

  4. mysql的密码忘记了怎么办

    我们的大脑不是电脑,有些东西难免会忘,但是会了这个再也不担心宝宝忘记密码了 (1)点击开始/控制面板/服务/mysql-->右击mysql看属性,里面有mysql的安装地址,然后找到安装地址进行 ...

  5. [Microsoft][ODBC 驱动程序管理器] 未发现数据源名称并且未指定默认驱动程序

    2003的access数据库文件后缀是mdb2007的access数据库文件后缀是accdb 我装的access2010所以驱动程序选择“Microsoft Access Driver (*.mdb, ...

  6. Java虚拟机的最大内存是多少

    天分析了当前比较流行的几个不同公司不同版本JVM的最大内存,得出来的结果如下: 公司 JVM版本 最大内存(兆)client 最大内存(兆)server SUN 1.5.x 1492 1520 SUN ...

  7. Android Studio下加入百度地图的使用(二)——定位服务

    上一章(http://www.cnblogs.com/jerehedu/p/4891216.html)中我们已经完成了环境的搭建,这一章我们来研究一下如何使用. 第一步:在xml文件中加入以下权限 & ...

  8. Kafka:ZK+Kafka+Spark Streaming集群环境搭建(三十):使用flatMapGroupsWithState替换agg

    flatMapGroupsWithState的出现解决了什么问题: flatMapGroupsWithState的出现在spark structured streaming原因(从spark.2.2. ...

  9. CentOS7 命令笔记

    网络 ifconfig已经过时,查看ip地址请使用ip addr或者ip link 服务 查看系统和硬件信息 cat /etc/os-release uname -r 显示正在使用的内核版本 dmid ...

  10. wifipineapple外接SD卡

    通过SSH或者web访问URL, http://172.16.42.1:1471 输入帐号:root  密码:pineapplesareyummy(默认账号密码) ssh连接:ssh root@172 ...