简介
一个流表示一个元素的序列,并支持不同类型的操作,以达到预期的结果。流的源头通常是一个集合或一个数组,数据从那里流出来。
流在几个方面与集合不同;最明显的是,流不是一个存储元素的数据结构。它们在本质上是功能性的,值得注意的是,对一个流的操作会产生一个结果,通常会返回另一个流,但不会修改其来源。
为了 "巩固 "这些变化,你把一个流的元素收集回一个Collection 。
在本指南中,我们将看看如何在Java 8中收集流元素到地图中。
采集器和Stream.collect()
收集器代表了Collector 接口的实现,它实现了各种有用的缩减操作,例如将元素累积到集合中,根据特定参数总结元素,等等。
所有预定义的实现都可以在
Collectors类中找到。
你也可以非常容易地实现你自己的收集器,并使用它来代替预定义的收集器--你可以用内置的收集器走得很远,因为它们涵盖了你可能想要使用它们的绝大多数情况。
为了能够在我们的代码中使用这个类,我们需要导入它。
import static java.util.stream.Collectors.*;
Stream.collect() 在流的元素上执行一个可变的还原操作。
可变还原操作在处理流中的元素时,将输入元素收集到一个可变容器中,比如Collection 。
Collectors.toMap()的指南
在Collectors 类中的许多其他方法中,我们还可以找到toMap() 的系列方法。toMap() 方法有三个重载的变体,其中有一对强制性的Mapper函数和可选的Merge函数和Supplier函数。
当然,这三种方法都返回一个
Collector,将元素累积到一个Map,其键和值是对输入元素应用所提供的(强制和可选)函数的结果。
根据我们所使用的重载,每个toMap() 方法都需要不同数量的参数,这些参数建立在之前的重载实现之上。稍后我们会更多地讨论这些差异。
让我们首先定义一个有几个字段的简单类,以及一个经典的构造函数、获取器和设置器。
private String name;
private String surname;
private String city;
private double avgGrade;
private int age;
// Constructors, Getters, Setters, toString()
平均成绩是一个double ,范围是6.0 - 10.0 。
让我们实例化一个我们将在接下来的例子中使用的学生的List 。
List<Student> students = Arrays.asList(
new Student("John", "Smith", "Miami", 7.38, 19),
new Student("Mike", "Miles", "New York", 8.4, 21),
new Student("Michael", "Peterson", "New York", 7.5, 20),
new Student("James", "Robertson", "Miami", 9.1, 20),
new Student("Kyle", "Miller", "Miami", 9.83, 20)
);
*收集器.toMap()*与映射器函数
该方法的基本形式只是需要两个映射器函数--一个keyMapper 和valueMapper 。
public static <T,K,U> Collector<T,?,Map<K,U>>
toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper)
该方法很简单--keyMapper 是一个映射函数,其输出是最终Map 的键。valueMapper 是一个映射函数,其输出是最终Map 的值。该方法的返回值是一个Collector ,它将元素收集到一个Map ,其对<K, V> 是先前应用的映射函数的结果。
我们先把我们的学生流转化为一个Map 。对于第一个例子,我们说我们想把学生的名字映射到他们的平均成绩,也就是创建一个<K, V> 对,它有一个<name, avgGrade> 的形式。
对于keyMapper ,我们将提供一个与返回姓名的方法相对应的函数,而对于valueMapper ,我们将提供一个与返回学生平均成绩的方法相对应的函数。
Map<String, Double> nameToAvgGrade = students.stream()
.collect(Collectors.toMap(Student::getName, Student::getAvgGrade));
请注意,Student::getName 只是一个方法参考--λ表达式student -> student.getName() 的速记表示法。
运行这段代码的结果是包含一个地图。
{Mike=8.4, James=9.1, Kyle=9.83, Michael=7.5, John=7.38}
如果我们想把整个特定的Student 对象只映射到它们的名字上,会怎么样?Java提供了一个来自Function 接口的内置identity() 方法。这个方法简单地返回一个函数,该函数总是返回其输入参数。
也就是说--我们可以很容易地将每个对象的身份(对象本身)映射到它们的名字。
Map<String, Student> nameToStudentObject = students.stream()
.collect(Collectors.toMap(Student::getName, Function.identity()));
**注意:**另外,我们可以不使用Function.identity() ,而是简单地使用一个Lambda表达式,element -> element ,它只是将每个element 映射到它自己。
在这里,Student::getName 是我们的keyMapper 函数,Function.identity() 是我们的valueMapper 函数,创建一个包含地图的。
{
Mike=Student{name='Mike', surname='Miles', city='New York', avgGrade=8.4, age=21},
James=Student{name='James', surname='Robertson', city='Miami', avgGrade=9.1, age=20},
Kyle=Student{name='Kyle', surname='Miller', city='Miami', avgGrade=9.83, age=20},
Michael=Student{name='Michael', surname='Peterson', city='New York', avgGrade=7.5, age=20},
John=Student{name='John', surname='Smith', city='Miami', avgGrade=7.38, age=19}
}
当然,这个输出在视觉上并不像我们将学生的名字映射到他们的平均成绩时那样干净,但这只是取决于Student 类的toString() 。
尽管这个特殊的重载是最容易使用的,但它在一个非常重要的部分--重复的关键元素上有所欠缺。例如,如果我们有两个名为"John" 的学生,而我们想像上面的例子那样将我们的List 转换为Map ,我们会遇到一个明显的问题。
Exception in thread "main" java.lang.IllegalStateException: Duplicate key John (attempted merging values 7.38 and 8.93)
关键是--该方法试图合并这两个值,并将合并后的值分配给唯一键--"John" ,结果失败了。我们可以决定提供一个合并函数,定义在存在重复键的情况下应该如何进行这种合并。
如果你想摆脱重复的键,你总是可以在收集之前向Stream添加一个distinct() 操作即可。
Map<String, Double> nameToStudentObject = students.stream()
.distinct()
.collect(Collectors.toMap(Student::getName, Student::getAvgGrade));
*Collectors.toMap()*与Mapper和Merge函数
除了两个Mapper函数,我们还可以提供一个Merge函数。
public static <T,K,U> Collector<T,?,Map<K,U>>
toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper,
BinaryOperator<U> mergeFunction)
mergeFuction 是一个函数,只有在我们最终的Map 中存在重复的关键元素,需要将它们的值合并并分配给一个唯一的键时才会被调用。它的输入是两个值,也就是keyMapper 返回相同的键的两个值,并将这两个值合并成一个。
**注意:**如果你有更多的两个非唯一键的值,第一次合并的结果被认为是第二次合并的第一个值,以此类推。
让我们添加另一个John ,来自另一个城市,有不同的平均成绩。
new Student("John Smith", "Las Vegas", 8.93,19)...
现在棘手的部分来了--我们如何处理重复的即冲突的键?我们需要确切地说明我们要如何处理这种情况。你可以决定只是用distinct() ,抛出一个异常来引发一个明显的警报*,或者*定义一个合并的策略来修剪重复的值。
修剪元素可能不是你想要的,因为它可能导致无声的失败,在最终的地图中缺少某些元素。更多的时候,我们抛出一个IllegalStateException!mergeFunction BinaryOperator ,而这两个元素被表示为(a, b) 。
如果你要抛出一个异常,你不会真正使用它们(除非用于记录或显示消息),所以我们可以直接在代码块中抛出异常。
Map<String, Double> nameToAvgGrade = students.stream()
.collect(Collectors.toMap(
Student::getName,
Student::getAvgGrade,
(a, b) ->
{ throw new IllegalStateException("Duplicate key");})
);
这将在代码运行时抛出一个异常。
Exception in thread "main" java.lang.IllegalStateException: Duplicate key
第二个解决方案是实际定义一个合并策略。例如,你可以取新的值,b ,或者保留旧的值,a 。或者,你可以计算出它们的平均值,并将其分配给它们。
Map<String, Double> nameToAvgGrade = students.stream()
.collect(Collectors.toMap(Student::getName,
Student::getAvgGrade,
(a, b) -> { return (a+b)/2;})
// Or (a, b) -> (a+b)/2
);
现在,当有重复的键出现时,它们的平均值被分配给最终地图中的唯一键。
**注意:**正如你所看到的--合并函数并不真的需要合并什么。实际上,它可以是任何函数,甚至是那些完全无视两个运算符的函数,比如抛出一个异常。
运行这段代码的结果是一个包含的地图。
{Mike=8.4, Kyle=9.83, James=9.1, Michael=7.5, John=8.155}
这个解决方案可能对你来说很好,也可能不是。当冲突发生时,我们一般会停止执行或以某种方式修剪数据,但Java本身并不支持多图的概念,在多图中,多个值可以分配给同一个键。
然而,如果你不介意使用外部库,如Guava或Apache Commons Collections,它们都支持多图的概念,分别名为
Multimap和MultiValuedMap。
Collectors.toMap()带有映射器、合并和供应商函数
该方法的最终重载版本接受一个Supplier 函数--它可以用来提供一个新的Map 接口的实现,以 "将结果打包进去"。
public static <T,K,U,M extends Map<K,U>> Collector<T,?,M>
toMap(Function<? super T,? extends K> keyMapper,
Function<? super T,? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier)
mapSupplier 函数指定了我们想要使用的Map 的特定实现,作为我们最终的Map 。当我们使用Map 来声明我们的地图时,Java默认使用一个HashMap 作为实现来存储它们。
这通常是很好的,这也是为什么它是默认的实现。然而,有时候,HashMap 的特性可能不适合你。例如,如果你想保持一个流中元素的原始顺序,或者通过中间流操作对它们进行排序,HashMap 不会保留这个顺序,而是根据对象的哈希值进行分类。那么--你可能会选择使用LinkedHashMap ,来代替保留这个顺序。
为了提供一个供应商,你必须同时提供一个合并函数。
Map<String, Double> nameToAvgGrade = students.stream()
.collect(Collectors.toMap(Student::getName,
Student::getAvgGrade,
(a, b) -> (a+b)/2,
LinkedHashMap::new)
);
运行代码的输出
{John=8.155, Mike=8.4, Michael=7.5, James=9.1, Kyle=9.83}
由于我们使用了LinkedHashMap ,原始List 中的元素的顺序在我们的Map 中保持不变,而不是让HashMap 来决定位置,从而得到分档的输出。
{Mike=8.4, Kyle=9.83, James=9.1, Michael=7.5, John=8.155}
结论
在本指南中,我们已经看了如何在Java中把一个流转换为一个地图--用一对Mapper函数、一个Merge函数和一个Supplier。