本系列文章翻译自@shekhargulati的java8-the-missing-tutorial

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

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

  • 将一个数据流中的元素进行分组:根据任务类型将流中所有的任务进行分组。这将产生一个Map<TaskType, List

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

Collector实际应用

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

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

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

public static void main(String[] args) {List<Task> tasks = getTasks();Map<TaskType, List<Task>> allTasksByType = new HashMap<>();for (Task task : tasks) {List<Task> existingTasksByType = allTasksByType.get(task.getType());if (existingTasksByType == null) {List<Task> tasksByType = new ArrayList<>();tasksByType.add(task);allTasksByType.put(task.getType(), tasksByType);} else {existingTasksByType.add(task);}}for (Map.Entry<TaskType, List<Task>> entry : allTasksByType.entrySet()) {System.out.println(String.format("%s =>> %s", entry.getKey(), entry.getValue()));}
}

收集器:常用的规约操作

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

缩减为一个值

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

将数据收集进一个列表

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

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

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

将数据收集进一个集合

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

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

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

将数据收集进一个映射

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

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

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

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

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

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

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

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

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

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

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

使用其它的收集器

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

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

找到拥有最长标题的任务

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

统计标签的总数

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

生成任务标题的概述

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

分类收集器

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

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

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

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

它将会产生如下的输出。

{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:根据标签分类

private static Map<String, List<Task>> groupingByTag(List<Task> tasks) {return tasks.stream().flatMap(task -> task.getTags().stream().map(tag -> new TaskTag(tag, task))).collect(groupingBy(TaskTag::getTag, mapping(TaskTag::getTask,toList())));
}private static class TaskTag {final String tag;final Task task;public TaskTag(String tag, Task task) {this.tag = tag;this.task = task;}public String getTag() {return tag;}public Task getTask() {return task;}}

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

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

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

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

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

分割

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

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

生成统计信息

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

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

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

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

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

连接所有的标题

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

编写一个定制的收集器

import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;public class MultisetCollector<T> implements Collector<T, Multiset<T>, Multiset<T>> {@Overridepublic Supplier<Multiset<T>> supplier() {return HashMultiset::create;}@Overridepublic BiConsumer<Multiset<T>, T> accumulator() {return (set, e) -> set.add(e, 1);}@Overridepublic BinaryOperator<Multiset<T>> combiner() {return (set1, set2) -> {set1.addAll(set2);return set1;};}@Overridepublic Function<Multiset<T>, Multiset<T>> finisher() {return Function.identity();}@Overridepublic Set<Characteristics> characteristics() {return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));}
}import com.google.common.collect.Multiset;import java.util.Arrays;
import java.util.List;public class MultisetCollectorExample {public static void main(String[] args) {List<String> names = Arrays.asList("shekhar", "rahul", "shekhar");Multiset<String> set = names.stream().collect(new MultisetCollector<>());set.forEach(str -> System.out.println(str + ":" + set.count(str)));}
}

Java8中的字数统计

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

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

Java8学习笔记(七)--Collectors相关推荐

  1. Typescript 学习笔记七:泛型

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

  2. Java8学习笔记(1) -- 从函数式接口说起

    转载自   Java8学习笔记(1) -- 从函数式接口说起 希望本文能够成为Java8 Lambda表达式的快速入门指南. 函数式接口 理解Functional Interface(函数式接口,以下 ...

  3. 吴恩达《机器学习》学习笔记七——逻辑回归(二分类)代码

    吴恩达<机器学习>学习笔记七--逻辑回归(二分类)代码 一.无正则项的逻辑回归 1.问题描述 2.导入模块 3.准备数据 4.假设函数 5.代价函数 6.梯度下降 7.拟合参数 8.用训练 ...

  4. websocket 获取连接id_Swoole学习笔记七:搭建WebSocket长连接 之 使用 USER_ID 作为身份凭证...

    Swoole学习笔记七:搭建WebSocket长连接 之 使用 USER_ID 作为身份凭证 2年前 阅读 3678 评论 0 喜欢 0 ### 0.前言 前面基本的WebSocket操作,我们基本都 ...

  5. ROS学习笔记七:使用rqt_console和roslaunch

    ROS学习笔记七:使用rqt_console和roslaunch 本节主要介绍在调试时使用的rqt_console和rqt_logger_level,以及一次性打开多个节点的工具roslaunch. ...

  6. 【K210】K210学习笔记七——使用K210拍摄照片并在MaixHub上进行训练

    [K210]K210学习笔记七--使用K210拍摄照片并在MaixHub上进行训练 前言 K210准备工作 K210如何拍摄照片 准备工作 拍摄相关代码定义 用K210拍摄到的照片在MaixHub平台 ...

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

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

  8. window的dos命令学习笔记 七

    文章目录 一.dos历史学习笔记(后期整合到这里,我想能学到这里的应该不多了,嘿嘿,加油) 二.执行状态返回值(`%errorlevel%`,和shell中`$?`相似): 三.视窗 1.color ...

  9. ESP32学习笔记(七) 复位和时钟

    ESP32学习笔记(七) 复位和时钟 目录: ESP32学习笔记(一) 芯片型号介绍 ESP32学习笔记(二) 开发环境搭建 VSCode+platformio ESP32学习笔记(三) 硬件资源介绍 ...

  10. 逆向脱壳破解分析基础学习笔记七 堆栈图(重点)

    本文为本人 大神论坛 逆向破解脱壳学习笔记之一,为本人对以往所学的回顾和总结,可能会有谬误之处,欢迎大家指出. 陆续将不断有笔记放出,希望能对想要入门的萌新有所帮助,一起进步 堆栈图 首先给定一段反汇 ...

最新文章

  1. 干货 | 使用FFT变换自动去除图像中严重的网纹
  2. 【Rollo的Python之路】Python 同步条件 学习笔记 Event
  3. ios8改变statusBar字体的显示颜色
  4. Spring Boot 多环境配置(properties和yaml方法的比较)
  5. 先进先出算法_数据结构与算法之初识栈与队列
  6. OpenCV总结——convertTo函数与浮点数类型
  7. java 泛型去重_泛型,list集合去重
  8. 计算两个时间之间的进度百分比
  9. Android多开/分身检测
  10. C语言男性标准体重,2019男人标准体重表!
  11. linux宕机故障分析案例,[文章]Linux宕机故障分析案例
  12. 两年后,中国CF又站在了世界之巅
  13. Bzoj5109: [CodePlus 2017]大吉大利,晚上吃鸡!
  14. IRF(智能弹性架构)
  15. Android 开发中命名规则
  16. html颜色参考 速查 在线取色,Color by Fardos - 配色/取色插件
  17. 我的世界海洋java_我的世界Minecraft Java版18w15a发布
  18. 插入页眉和页脚-----------毕业设计必备攻略
  19. 中考前 OI 生涯总结
  20. 构建企业级云原生日志系统架构

热门文章

  1. [我研究] A TAXONOMY OF SECURITY FAULTS IN THE UNIX OPERATING SYSTEM - Master Thesis
  2. commit是直接提交到远程吗 svn_xcode 把项目代码提交到远程SVN服务器
  3. java model 封装_Java封装统一的Result Model案例
  4. 小程序向java后台发送图片_微信小程序在后台如何将二进制流转换成图片
  5. mysql手机号11_我用不小心用 mysql 的int(11) 存了 手机号,数据都有问题,有办法恢复么?...
  6. float去掉小数点之后_float类型的存储方式
  7. 联想Y 系列 四面壳展示
  8. Java三维文字特效设计_jQuery实现3D文字特效的方法
  9. ElasticSearch 之中文分词器
  10. 面试中该如何介绍项目?