一个流表示一个
元素的序列
,并支持不同类型的操作,以达到预期的结果。流的
源头
通常是一个
集合
或一个
数组
,数据从那里流出来。
流在几个方面与集合不同;最明显的是,流不是一个存储元素的
数据结构
。它们在本质上是功能性的,值得注意的是,对一个流的操作会产生一个结果,通常会返回另一个流,但不会修改其来源。
为了 "巩固 "这些变化,你把一个流的元素
收集
回一个
Collection
。
收集器和Stream.collect()
收集器
代表了
Collector
接口的实现,它实现了各种有用的减少操作,例如将元素累积到集合中,根据特定的参数总结元素,等等。
所有预定义的实现都可以在
Collectors
类中找到。
你也可以非常容易地实现你自己的收集器,并使用它来代替预定义的收集器--你可以用内置的收集器走得很远,因为它们涵盖了你可能想要使用它们的绝大多数情况。
为了能够在我们的代码中使用这个类,我们需要导入它。
import static java.util.stream.Collectors.*;
Stream.collect()
在流的元素上执行一个可变的还原操作。
可变还原操作在处理流中的元素时,将输入元素收集到一个可变的容器中,例如Collection
。
在本指南中,我们将经常使用Stream.collect()
,并与Collectors.groupingBy()
采集器配对。
收集器.groupingBy()
Collectors
类是庞大而通用的,它的许多方法之一,也是本文的主要话题,就是Collectors.groupingBy()
。这个方法给我们提供了类似于SQL中*"GROUP BY "*语句的功能。
我们使用Collectors.groupingBy()
,通过给定的特定属性对对象进行分组,并将最终结果存储在一个地图中。
让我们定义一个有几个字段的简单类,以及一个经典的构造函数和getters/setters。我们将使用这个类来按主题、城市和年龄对Student
s的实例进行分组。
public class Student {
private String subject;
private String name;
private String surname;
private String city;
private int age;
让我们实例化一个我们将在接下来的例子中使用的学生的List
。
List<Student> students = Arrays.asList(
new Student("Math", "John", "Smith", "Miami", 19),
new Student("Programming", "Mike", "Miles", "New York", 21),
new Student("Math", "Michael", "Peterson", "New York", 20),
new Student("Math", "James", "Robertson", "Miami", 20),
new Student("Programming", "Kyle", "Miller", "Miami", 20)
Collectors.groupingBy()
方法在Collectors
类中有三个重载--每个重载都建立在其他重载之上。我们将在接下来的章节中一一介绍。
带有分类功能的Collectors.groupingBy()
Collectors.groupingBy()
方法的第一个变体只需要一个参数--一个分类函数。它的语法如下。
public static <T,K> Collector<T,?,Map<K,List<T>>>
groupingBy(Function<? super T,? extends K> classifier)
该方法返回一个Collector
,根据分类函数对输入的T
类型的元素进行分组,并将结果返回到一个Map
。
分类函数将元素映射到一个类型为K
的键。正如我们所提到的,收集器制作一个Map<K, List<T>>
,其键是在输入元素上应用分类函数所产生的值。这些键的值是Lists
,其中包含映射到相关键的输入元素。
这是三个变体中最简单的一个。并不是说其他的更难理解,只是这个具体的实现需要的参数最少。
让我们把我们的学生按科目分成几组。
Map<String, List<Student>> studentsBySubject = students
.stream()
.collect(
Collectors.groupingBy(Student::getSubject)
这一行执行后,我们有一个Map<K, V>
,在我们的例子中,K
将是Math
或Programming
,而V
代表一个Student
对象的List
,这些对象被映射到学生目前正在学习的科目K
。现在,如果我们只是打印我们的studentBySubject
地图,我们会看到两组,每组有几个学生。
Programming=[Student{name='Mike', surname='Miles'}, Student{name='Kyle', surname='Miller'}],
Math=[Student{name='John', surname='Smith'}, Student{name='Michael', surname='Peterson'}, Student{name='James', surname='Robertson'}]
我们可以看到,这与我们预期的结果有些相似--目前有2个学生在上编程课,3个在上数学课。
使用分类函数和下游收集器的collectors.groupingBy()
当仅仅是分组还不够时--你也可以为groupingBy()
方法提供一个下游收集器。
public static <T,K,A,D> Collector<T,?,Map<K,D>>
groupingBy(Function<? super T,? extends K> classifier,
Collector<? super T,A,D> downstream)
该方法返回一个Collector
,该方法根据分类函数对类型为T
的输入元素进行分组,之后使用指定的下游Collector
,对与给定键相关的值进行还原操作。
如前所述,还原操作通过应用一个在特定情况下有用的操作来 "减少 "我们收集的数据。
在这个例子中,我们想按照学生所在的城市来分组,但不是整个Student
对象。假设我们只想收集他们的名字(把他们还原成一个名字)。
作为这里的下游,我们将使用Collectors.mapping()
方法,它需要2个参数。
一个映射器 - 一个应用于输入元素的函数,以及
一个下游收集器--一个将接受映射值的收集器
Collectors.mapping()
它本身做了一个非常简单的工作。它通过在累积之前对每个输入元素应用映射函数,使接受一种类型的元素的收集器适应于接受不同的类型。在我们的例子中,我们将把每个 映射到它们的名字上,并将这些名字作为一个列表返回。Student
为了简单起见,因为我们的ArrayList
中只有5个学生,我们只有迈阿密和纽约这两个城市。为了按照前面提到的方式对学生进行分组,我们需要运行以下代码。
Map<String, List<String>> studentsByCity = students.stream()
.collect(Collectors.groupingBy(
Student::getCity,
Collectors.mapping(Student::getName, Collectors.toList())));
System.out.println(studentsByCity);
**注意:**例如,我们可以不使用List<String>
,而使用Set<String>
。如果我们选择这样做,我们还需要将代码中的toList()
部分替换为toSet()
。
这一次,我们将有一个城市的Map
,并有一个与城市相关的学生名字列表。这些是学生的缩减,我们把他们缩减为一个名字,当然你也可以用其他的缩减操作代替。
{New York=[Mike, Michael], Miami=[John, James, Kyle]}
Collectors.groupingBy() 与 Collectors.counting()
同样,还原操作是非常强大的,可以用来寻找最小、最大、平均、总和,以及将集合还原成更小的内聚整体。
例如,我们可以不把学生还原成他们的名字,而是把学生列表还原成他们的数量,这可以通过Collectors.counting()
作为还原操作的封装器轻松实现。
Map<Integer, Long> countByAge = students.stream()
.collect(Collectors.groupingBy(
Student::getAge,
Collectors.counting()))
countByAge
地图现在将包含学生的分组,按年龄分组,这些键的值将是每组学生的数量。
{19=1, 20=3, 21=1}
同样,你可以用还原操作做各种各样的事情,这只是其中的一个方面。
多重收集器.groupingBy()
下游收集器的一个类似而又强大的应用是,我们可以做另一个Collectors.groupingBy()
。
假设我们想先按年龄(大于20岁的)过滤所有的学生,然后按年龄分组。每个组都会有额外的学生组,按他们的城市分组。
20={New York=[Student{name='Michael', surname='Peterson'}], Miami=[Student{name='James', surname='Robertson'}, Student{name='Kyle', surname='Miller'}]},
21={New York=[Student{name='Mike', surname='Miles'}]}
带有分类函数、下游收集器和供应商的collectors.groupingBy()
第三个也是最后一个重载的groupingBy()
方法变体与之前的两个参数相同,但多了一个--供应商方法。
这个方法提供了我们想要用来包含我们最终结果的特定Map
实现。
public static <T,K,D,A,M extends Map<K,D>> Collector<T,?,M>
groupingBy(Function<? super T,? extends K> classifier,
Supplier<M> mapFactory,
Collector<? super T,A,D> downstream)
这个实现与之前的实现只有一点不同,无论是在代码还是在作品上。它返回一个Collector
,根据分类函数对输入元素的类型T
,之后使用指定的下游Collector
,对与给定键相关的值进行还原操作。同时,Map
是使用提供的mapFactory
供应商实现的。
对于这个例子,我们也只是修改前面的例子。
Map<String, List<String>> namesByCity = students.stream()
.collect(Collectors.groupingBy(
Student::getCity,
TreeMap::new,
Collectors.mapping(Student::getName, Collectors.toList())));
**注意:**我们可以使用Java提供的任何其他Map
实现--比如HashMap
或LinkedHashMap
也可以。
简而言之,这段代码将给我们一个按城市分组的学生列表,由于我们在这里使用的是TreeMap
,城市的名字将被排序。
与先前唯一不同的是,我们添加了另一个参数--TreeMap::new
,它指定了我们要使用的Map
的确切实现。
{Miami=[John, James, Kyle], New York=[Mike, Michael]}
这使得收集流到地图的过程要比使用不同的实现方式再次流和重新插入元素要容易得多,比如说。
Map<String, List<String>> namesByCity = students.stream().collect(Collectors.groupingBy(
Student::getCity,
Collectors.mapping(Student::getName, Collectors.toList())))
.entrySet()
.stream()
.sorted(comparing(e -> e.getKey()))
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(a, b) -> {
throw new AssertionError();
LinkedHashMap::new
当你使用Supplier
,像这样冗长、曲折、多流的代码完全可以用更简单的重载版本取代。
这段代码的结果也与之前的输出相同。
{Miami=[John, James, Kyle], New York=[Mike, Michael]}
Collectors
类是一个强大的类,它允许我们以各种方式将流收集到集合中。
你可以定义你自己的收集器,但内置的收集器可以让你走得很远,因为它们是通用的,可以通用于你能想到的绝大部分任务。
在本指南中,我们已经看了groupingBy()
收集器,它基于分类函数(通常归结为对象的一个字段)对实体进行分组,以及它的重载变体。
你已经学会了如何使用基本的表单,以及与下游收集器和供应商一起使用表单来简化代码,并在流上运行强大而简单的功能操作。