Java8新特性之Collectors
在第二天,你已经学习了Stream API能够让你以声明式的方式帮助你处理集合。我们看到collect
是一个将管道流的结果集到一个list
中的结束操作。collect
是一个将数据流缩减为一个值的归约操作。这个值可以是集合、映射,或者一个值对象。你可以使用collect
达到以下目的:
将数据流缩减为一个单一值:一个流执行后的结果能够被缩减为一个单一的值。单一的值可以是一个
Collection
,或者像int、double等的数值,再或者是一个用户自定义的值对象。将一个数据流中的元素进行分组:根据任务类型将流中所有的任务进行分组。这将产生一个
Map<TaskType, List<Task>>
的结果,其中每个实体包含一个任务类型以及与它相关的任务。你也可以使用除了列表以外的任何其他的集合。如果你不需要与一任务类型相关的所有的任务,你可以选择产生一个Map<TaskType, Task>
。这是一个能够根据任务类型对任务进行分类并获取每类任务中第一个任务的例子。分割一个流中的元素:你可以将一个流分割为两组——比如将任务分割为要做和已经做完的任务。
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()); //
System.out.println(summaryStatistics.getMax()); //
System.out.println(summaryStatistics.getMin()); //
System.out.println(summaryStatistics.getSum()); //
也有其它的变种形式,像针对其它原生类型的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>> { @Override
public Supplier<Multiset<T>> supplier() {
return HashMultiset::create;
} @Override
public BiConsumer<Multiset<T>, T> accumulator() {
return (set, e) -> set.add(e, 1);
} @Override
public BinaryOperator<Multiset<T>> combiner() {
return (set1, set2) -> {
set1.addAll(set2);
return set1;
};
} @Override
public Function<Multiset<T>, Multiset<T>> finisher() {
return Function.identity();
} @Override
public 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的更多相关文章
- java8 array、list操作 汇【3】)(-Java8新特性之Collectors 详解
//编写一个定制的收集器 public static class MultisetCollector<T> implements Collector<T, Multiset<T ...
- java8新特性全面解析
在Java Code Geeks上有大量的关于Java 8 的教程了,像玩转Java 8--lambda与并发,Java 8 Date Time API 教程: LocalDateTime和在Java ...
- Java8 新特性之Stream----java.util.stream
这个包主要提供元素的streams函数操作,比如对collections的map,reduce. 例如: int sum = widgets.stream() .filter(b -> b.ge ...
- 这可能是史上最好的 Java8 新特性 Stream 流教程
本文翻译自 https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-examples/ 作者: @Winterbe 欢迎关注个人微信公众 ...
- Java8 新特性之流式数据处理
一. 流式处理简介 在我接触到java8流式处理的时候,我的第一感觉是流式处理让集合操作变得简洁了许多,通常我们需要多行代码才能完成的操作,借助于流式处理可以在一行中实现.比如我们希望对一个包含整数的 ...
- java8新特性--Stream的基本介绍和使用
什么是Stream? Stream是一个来自数据源的元素队列并可以进行聚合操作. 数据源:流的来源. 可以是集合,数组,I/O channel, 产生器generator 等 聚合操作:类似SQL语句 ...
- java8新特性: lambda表达式:直接获得某个list/array/对象里面的字段集合
java8新特性: lambda表达式:直接获得某个list/array/对象里面的字段集合 比如,我有一张表: entity Category.java service CategoryServic ...
- 乐字节-Java8新特性-接口默认方法之Stream流(下)
接上一篇:<Java8新特性之stream>,下面继续接着讲Stream 5.流的中间操作 常见的流的中间操作,归为以下三大类:筛选和切片流操作.元素映射操作.元素排序操作: 操作 描述 ...
- Java8 新特性之流式数据处理(转)
转自:https://www.cnblogs.com/shenlanzhizun/p/6027042.html 一. 流式处理简介 在我接触到java8流式处理的时候,我的第一感觉是流式处理让集合操作 ...
随机推荐
- ④---在Eclipse中导入GIT项目
Eclipse中导入GIT项目 以下将为大家介绍如何在Eclipse中导入GIT项目. 一.在Eclipse中安装GIT 1首先打开Eclipse,然后点击Help>Install New So ...
- 初学Python——文件操作
一.文件的打开和关闭 1.常用的打开关闭语句 f=open("yesterday","r",encoding="utf-8") #打开文件 ...
- 在Ubuntu上快速搭建基于Beego的RESTful API
最近在研究Go,打算基于Go做点Web API,于是经过初步调研,打算用Beego这个框架,然后再结合其中提供的ORM以及Swagger的集成,可以快速搭建一个RESTful API的网站. 下面是具 ...
- python logging详解及自动添加上下文信息
之前写过一篇文章日志的艺术(The art of logging),提到了输出日志的时候记录上下文信息的重要性,我认为上下文信息包括: when:log事件发生的时间 where:log事件发生在哪个 ...
- Linux下如何让jar包程序在后台一直执行
Linux下如何让Jar包程序在后台一直执行 shell命令 nohup java -jar xxx.jar & &:让程序后台执行. nohub:让程序控制台输出转移到nohub.o ...
- C# 中的相对路径在 Picturebox 中的应用
前言 最近的项目需要将 picturebox 显示网络图片的小功能完成,不想用绝对路径取本地文件里的图片,因为将来要发布给用户的话让用户自己配置会很麻烦的,索性将路径设置成相对路径,将图片放在自己的项 ...
- C#中存储数据的集合:数组、集合、泛型、字典
为什么把这4个东西放在一起来说,因为c#中的这4个对象都是用来存储数据的集合……. 首先咱们把这4个对象都声明并实例化一下: //数组 ]; //集合 ArrayList m_AList = new ...
- 52abp框架asp.net core & Angular快速开发实战视频教程
课程标题 52abp框架asp.net core & Angular全栈开发实战视频课程 课程简介 从零开始学 52ABP企业开发框架,企业项目是如何开发和技术选型,代码如何管理,团队协同开发 ...
- [LeetCode] 56 - Merge Intervals 合并区间
Given a collection of intervals, merge all overlapping intervals. For example,Given [1,3],[2,6],[8,1 ...
- public private protected default小结
public:可以被所有其他类所访问: private:只能被自己访问和修改: protected:自身.子类及同一个包中类可以访问: default:声明时没有加修饰符,同一包中的类可以访问:当子类 ...