Java 编程问题:九、函数式编程——深入研究3https://developer.aliyun.com/article/1426156
获取全部
有没有办法在一次幺正运算中获得计数、和、平均值、最小值和最大值?
是的,有!当我们需要两个或更多这样的操作时,我们可以依赖于Collectors.summarizingInt()
、summarizingLong()
和summarizingDouble()
。这些方法将这些操作分别包装在IntSummaryStatistics
、LongSummaryStatistics
和DoubleSummaryStatistics
中,如下所示:
IntSummaryStatistics melonWeightsStatistics = melons .stream().collect(Collectors.summarizingInt(Melon::getWeight));
打印此对象会产生以下输出:
IntSummaryStatistics{count=7, sum=15900, min=1600, average=2271.428571, max=3000}
对于每个操作,我们都有专门的获取器:
int max = melonWeightsStatistics.getMax()
我们都完了!现在,让我们来讨论如何对流的元素进行分组。
189 分组
假设我们有以下Melon
类和Melon
的List
:
public class Melon { enum Sugar { LOW, MEDIUM, HIGH, UNKNOWN } private final String type; private final int weight; private final Sugar sugar; // constructors, getters, setters, equals(), // hashCode(), toString() omitted for brevity } List<Melon> melons = Arrays.asList( new Melon("Crenshaw", 1200), new Melon("Gac", 3000), new Melon("Hemi", 2600), new Melon("Hemi", 1600), new Melon("Gac", 1200), new Melon("Apollo", 2600), new Melon("Horned", 1700), new Melon("Gac", 3000), new Melon("Hemi", 2600) );
JavaStream
API 通过Collectors.groupingBy()
公开了与 SQLGROUP BY
子句相同的功能。
当 SQLGROUP BY
子句作用于数据库表时,Collectors.groupingBy()
作用于流的元素。
换句话说,groupingBy()
方法能够对具有特定区别特征的元素进行分组。在流和函数式编程(java8)之前,这样的任务是通过一堆繁琐、冗长且容易出错的意大利面代码应用于集合的。从 Java8 开始,我们有分组收集器。
在下一节中,我们来看看单级分组和多级分组。我们将从单级分组开始。
单级分组
所有分组收集器都有一个分类函数(将流中的元素分为不同组的函数),主要是Function
函数式接口的一个实例。
流的每个元素(属于T
类型)都通过这个函数,返回的将是分类器对象(属于R
类型)。所有返回的R
类型代表一个Map
的键(K
,每组都是这个Map
中的一个值。
换句话说,关键字(K
是分类函数返回的值,值(V
是流中具有该分类值的元素的列表(K
)。所以,最终的结果是Map>
类型。
让我们看一个例子,为这个大脑的逗逗解释带来一些启示。本例依赖于groupingBy()
最简单的味道,即groupingBy(Function classifier)
。
那么,让我们按类型将Melon
分组:
Map<String, List<Melon>> byTypeInList = melons.stream() .collect(groupingBy(Melon::getType));
输出如下:
{ Crenshaw = [Crenshaw(1200 g)], Apollo = [Apollo(2600 g)], Gac = [Gac(3000 g), Gac(1200 g), Gac(3000 g)], Hemi = [Hemi(2600 g), Hemi(1600 g), Hemi(2600 g)], Horned = [Horned(1700 g)] }
我们也可以将Melon
按重量分组:
Map<Integer, List<Melon>> byWeightInList = melons.stream() .collect(groupingBy(Melon::getWeight));
输出如下:
{ 1600 = [Hemi(1600 g)], 1200 = [Crenshaw(1200 g), Gac(1200 g)], 1700 = [Horned(1700 g)], 2600 = [Hemi(2600 g), Apollo(2600 g), Hemi(2600 g)], 3000 = [Gac(3000 g), Gac(3000 g)] }
此分组如下图所示。更准确地说,这是Gac(1200 g)
通过分类函数(Melon::getWeight
的瞬间的快照:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjfDkQHn-1657285412200)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/f66eb284-6695-44a3-83c3-f787b1cd75a3.png)]
因此,在甜瓜分类示例中,一个键是Melon
的权重,它的值是包含该权重的所有Melon
对象的列表。
分类函数可以是方法引用或任何其他 Lambda。
上述方法的一个问题是存在不需要的重复项。这是因为这些值是在一个List
中收集的(例如,3000=[Gac(3000g), Gac(3000g)
。但我们可以依靠另一种口味的groupingBy()
,即groupingBy(Function classifier, Collector downstream)
,来解决这个问题。
这一次,我们可以指定所需的下游收集器作为第二个参数。所以,除了分类函数,我们还有一个下游收集器。
如果我们想拒绝复制品,我们可以使用Collectors.toSet()
,如下所示:
Map<String, Set<Melon>> byTypeInSet = melons.stream() .collect(groupingBy(Melon::getType, toSet()));
输出如下:
{ Crenshaw = [Crenshaw(1200 g)], Apollo = [Apollo(2600 g)], Gac = [Gac(1200 g), Gac(3000 g)], Hemi = [Hemi(2600 g), Hemi(1600 g)], Horned = [Horned(1700 g)] }
我们也可以按重量计算:
Map<Integer, Set<Melon>> byWeightInSet = melons.stream() .collect(groupingBy(Melon::getWeight, toSet()));
输出如下:
{ 1600 = [Hemi(1600 g)], 1200 = [Gac(1200 g), Crenshaw(1200 g)], 1700 = [Horned(1700 g)], 2600 = [Hemi(2600 g), Apollo(2600 g)], 3000 = [Gac(3000 g)] }
当然,在这种情况下,也可以使用distinct()
:
Map<String, List<Melon>> byTypeInList = melons.stream() .distinct() .collect(groupingBy(Melon::getType));
按重量计算也是如此:
Map<Integer, List<Melon>> byWeightInList = melons.stream() .distinct() .collect(groupingBy(Melon::getWeight));
好吧,没有重复的了,但是结果不是有序的。这个映射最好按键排序,所以默认的HashMap
不是很有用。如果我们可以指定一个TreeMap
而不是默认的HashMap
,那么问题就解决了。我们可以通过另一种口味的groupingBy()
,也就是groupingBy(Function classifier, Supplier mapFactory, Collector downstream)
。
这个风格的第二个参数允许我们提供一个Supplier
对象,它提供一个新的空Map
,结果将被插入其中:
Map<Integer, Set<Melon>> byWeightInSetOrdered = melons.stream() .collect(groupingBy(Melon::getWeight, TreeMap::new, toSet()));
现在,输出是有序的:
{ 1200 = [Gac(1200 g), Crenshaw(1200 g)], 1600 = [Hemi(1600 g)], 1700 = [Horned(1700 g)], 2600 = [Hemi(2600 g), Apollo(2600 g)], 3000 = [Gac(3000 g)] }
我们也可以有一个List
包含 100 个瓜的重量:
List<Integer> allWeights = new ArrayList<>(100);
我们想把这个列表分成 10 个列表,每个列表有 10 个权重。基本上,我们可以通过分组得到,如下(我们也可以应用parallelStream()
:
final AtomicInteger count = new AtomicInteger(); Collection<List<Integer>> chunkWeights = allWeights.stream() .collect(Collectors.groupingBy(c -> count.getAndIncrement() / 10)) .values();
现在,让我们来解决另一个问题。默认情况下,Stream
被分成一组List
。但是我们怎样才能将Stream
划分成一组List
,每个列表只包含瓜的类型,而不是Melon
实例?
嗯,转化一个流的元素通常是map()
的工作。但是在groupingBy()
中,这是Collectors.mapping()
的工作(更多细节可以在本章的“过滤、展开和映射收集器”部分找到):
Map<Integer, Set<String>> byWeightInSetOrdered = melons.stream() .collect(groupingBy(Melon::getWeight, TreeMap::new, mapping(Melon::getType, toSet())));
这一次,输出正是我们想要的:
{ 1200 = [Crenshaw, Gac], 1600 = [Hemi], 1700 = [Horned], 2600 = [Apollo, Hemi], 3000 = [Gac] }
好的,到目前为止,很好!现在,让我们关注一个事实,groupingBy()
的三种风格中有两种接受收集器作为参数(例如,toSet()
。这可以是任何收集器。例如,我们可能需要按类型对西瓜进行分组并计数。为此,Collectors.counting()
很有帮助(更多细节可以在“摘要收集器”部分找到):
Map<String, Long> typesCount = melons.stream() .collect(groupingBy(Melon::getType, counting()));
输出如下:
{Crenshaw=1, Apollo=1, Gac=3, Hemi=3, Horned=1}
我们也可以按重量计算:
Map<Integer, Long> weightsCount = melons.stream() .collect(groupingBy(Melon::getWeight, counting()));
输出如下:
{1600=1, 1200=2, 1700=1, 2600=3, 3000=2}
我们能把最轻和最重的瓜按种类分类吗?我们当然可以!我们可以通过Collectors.minBy()
和maxBy()
来实现这一点,这在摘要收集器部分有介绍:
Map<String, Optional<Melon>> minMelonByType = melons.stream() .collect(groupingBy(Melon::getType, minBy(comparingInt(Melon::getWeight))));
输出如下(注意,minBy()
返回一个Optional
:
{ Crenshaw = Optional[Crenshaw(1200 g)], Apollo = Optional[Apollo(2600 g)], Gac = Optional[Gac(1200 g)], Hemi = Optional[Hemi(1600 g)], Horned = Optional[Horned(1700 g)] }
我们也可以通过maxMelonByType()
实现:
Map<String, Optional<Melon>> maxMelonByType = melons.stream() .collect(groupingBy(Melon::getType, maxBy(comparingInt(Melon::getWeight))));
输出如下(注意,maxBy()
返回一个Optional
:
{ Crenshaw = Optional[Crenshaw(1200 g)], Apollo = Optional[Apollo(2600 g)], Gac = Optional[Gac(3000 g)], Hemi = Optional[Hemi(2600 g)], Horned = Optional[Horned(1700 g)] }
minBy()
和maxBy()
收集器采用Comparator
作为参数。在这些示例中,我们使用了内置的Comparator.comparingInt()
函数。从 JDK8 开始,java.util.Comparator
类增加了几个新的比较器,包括用于链接比较器的thenComparing()
口味。
此处的问题由应删除的选项表示。更一般地说,这类问题将继续使收集器返回的结果适应不同的类型。
嗯,特别是对于这类任务,我们有collectingAndThen(Collector downstream, Function finisher)
工厂方法。此方法采用的函数将应用于下游收集器(分页装订器)的最终结果。可按如下方式使用:
Map<String, Integer> minMelonByType = melons.stream() .collect(groupingBy(Melon::getType, collectingAndThen(minBy(comparingInt(Melon::getWeight)), m -> m.orElseThrow().getWeight())));
输出如下:
{Crenshaw=1200, Apollo=2600, Gac=1200, Hemi=1600, Horned=1700}
我们也可以使用maxMelonByType()
:
Map<String, Integer> maxMelonByType = melons.stream() .collect(groupingBy(Melon::getType, collectingAndThen(maxBy(comparingInt(Melon::getWeight)), m -> m.orElseThrow().getWeight())));
输出如下:
{Crenshaw=1200, Apollo=2600, Gac=3000, Hemi=2600, Horned=1700}
我们还可以在Map
中按类型对瓜进行分组。同样,我们可以依赖collectingAndThen()
来实现这一点,如下所示:
Map<String, Melon[]> byTypeArray = melons.stream() .collect(groupingBy(Melon::getType, collectingAndThen( Collectors.toList(), l -> l.toArray(Melon[]::new))));
或者,我们可以创建一个通用收集器并调用它,如下所示:
private static <T> Collector<T, ? , T[]> toArray(IntFunction<T[]> func) { return Collectors.collectingAndThen( Collectors.toList(), l -> l.toArray(func.apply(l.size()))); } Map<String, Melon[]> byTypeArray = melons.stream() .collect(groupingBy(Melon::getType, toArray(Melon[]::new)));
多级分组
前面我们提到过三种口味的groupingBy()
中有两种以另一个收集器为论据。此外,我们说,这可以是任何收集器。任何一个收集器,我们也指groupingBy()
。
通过将groupingBy()
传递到groupingBy()
,我们可以实现n
——层次分组或多层次分组。主要有n
级分类函数。
让我们考虑一下Melon
的以下列表:
List<Melon> melonsSugar = Arrays.asList( new Melon("Crenshaw", 1200, HIGH), new Melon("Gac", 3000, LOW), new Melon("Hemi", 2600, HIGH), new Melon("Hemi", 1600), new Melon("Gac", 1200, LOW), new Melon("Cantaloupe", 2600, MEDIUM), new Melon("Cantaloupe", 3600, MEDIUM), new Melon("Apollo", 2600, MEDIUM), new Melon("Horned", 1200, HIGH), new Melon("Gac", 3000, LOW), new Melon("Hemi", 2600, HIGH));
因此,每个Melon
都有一个类型、一个重量和一个糖分水平指示器。首先,我们要根据糖分指标(LOW
、MEDIUM
、HIGH
或UNKNOWN
(默认值))对西瓜进行分组。此外,我们想把西瓜按重量分组。这可以通过两个级别的分组来实现,如下所示:
Map<Sugar, Map<Integer, Set<String>>> bySugarAndWeight = melonsSugar.stream() .collect(groupingBy(Melon::getSugar, groupingBy(Melon::getWeight, TreeMap::new, mapping(Melon::getType, toSet()))));
输出如下:
{ MEDIUM = { 2600 = [Apollo, Cantaloupe], 3600 = [Cantaloupe] }, HIGH = { 1200 = [Crenshaw, Horned], 2600 = [Hemi] }, UNKNOWN = { 1600 = [Hemi] }, LOW = { 1200 = [Gac], 3000 = [Gac] } }
我们现在可以说,克伦肖和角重 1200 克,含糖量高。我们还有 2600 克的高含糖量半胱胺。
我们甚至可以在一个表中表示数据,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-48k0wj5d-1657285412201)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/6fdf13c0-edd7-4213-a615-26cdc3faed3e.png)]
现在,让我们学习分区。
190 分区
分区是一种分组类型,它依赖于一个Predicate
将一个流分成两组(一组用于true
和一组用于false
)。true
的组存储流中已通过谓词的元素,false
的组存储其余元素(未通过谓词的元素)。
此Predicate
代表划分的分类函数,称为划分函数。因为Predicate
被求值为boolean
值,所以分区操作返回Map
。
假设我们有以下Melon
类和Melon
的List
:
public class Melon { private final String type; private int weight; // constructors, getters, setters, equals(), // hashCode(), toString() omitted for brevity } List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 1200), new Melon("Gac", 3000), new Melon("Hemi", 2600), new Melon("Hemi", 1600), new Melon("Gac", 1200), new Melon("Apollo", 2600), new Melon("Horned", 1700), new Melon("Gac", 3000), new Melon("Hemi", 2600));
分区通过Collectors.partitioningBy()
完成。这个方法有两种风格,其中一种只接收一个参数,即partitioningBy(Predicate predicate)
。
例如,按 2000 克的重量将西瓜分成两份,可按以下步骤进行:
Map<Boolean, List<Melon>> byWeight = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000));
输出如下:
{ false=[Crenshaw(1200g),Hemi(1600g), Gac(1200g),Horned(1700g)], true=[Gac(3000g),Hemi(2600g),Apollo(2600g), Gac(3000g),Hemi(2600g)] }
分区优于过滤的优点在于分区保留了流元素的两个列表。
下图描述了partitioningBy()
如何在内部工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gp4ytVqB-1657285412201)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/cc4f001a-fe91-42f2-966b-9707181a41ff.png)]
如果我们想拒绝重复,那么我们可以依赖于其他口味的partitioningBy()
,比如partitioningBy(Predicate predicate, Collector downstream)
。第二个参数允许我们指定另一个Collector
来实现下游还原:
Map<Boolean, Set<Melon>> byWeight = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000, toSet()));
输出将不包含重复项:
{ false=[Horned(1700g), Gac(1200g), Crenshaw(1200g), Hemi(1600g)], true=[Gac(3000g), Hemi(2600g), Apollo(2600g)] }
当然,在这种情况下,distinct()
也会起作用:
Map<Boolean, List<Melon>> byWeight = melons.stream() .distinct() .collect(partitioningBy(m -> m.getWeight() > 2000));
也可以使用其他收集器。例如,我们可以通过counting()
对这两组中的每一组元素进行计数:
Map<Boolean, Long> byWeightAndCount = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000, counting()));
输出如下:
{false=4, true=5}
我们还可以计算没有重复的元素:
Map<Boolean, Long> byWeight = melons.stream() .distinct() .collect(partitioningBy(m -> m.getWeight() > 2000, counting()));
这一次,输出如下:
{false=4, true=3}
最后,partitioningBy()
可以与collectingAndThen()
结合,我们在分组段中介绍了这一点。例如,让我们按 2000 g 的重量对西瓜进行分区,并将每个分区中的西瓜保持最重的部分:
Map<Boolean, Melon> byWeightMax = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000, collectingAndThen(maxBy(comparingInt(Melon::getWeight)), Optional::get)));
输出如下:
{false=Horned(1700g), true=Gac(3000g)}
191 过滤、展开和映射收集器
假设我们有以下Melon
类和Melon
的List
:
public class Melon { private final String type; private final int weight; private final List<String> pests; // constructors, getters, setters, equals(), // hashCode(), toString() omitted for brevity } List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 2000), new Melon("Hemi", 1600), new Melon("Gac", 3000), new Melon("Hemi", 2000), new Melon("Crenshaw", 1700), new Melon("Gac", 3000), new Melon("Hemi", 2600));
JavaStream
API 提供了filtering()
、flatMapping()
和mapping()
,特别是用于多级降阶(如groupingBy()
或partitioningBy()
的下游)。
在概念上,filtering()
的目标与filter()
相同,flatMapping()
的目标与flatMap()
相同,mapping()
的目标与map()
相同。
filtering()
用户问题:我想把所有重 2000 克以上的西瓜都按种类分类。对于每种类型,将它们添加到适当的容器中(每种类型都有一个容器—只需检查容器的标签即可)。
通过使用filtering(Predicate predicate, Collector downstream)
,我们对当前收集器的每个元素应用谓词,并在下游收集器中累积输出。
因此,要将重量超过 2000 克的西瓜按类型分组,我们可以编写以下流管道:
Map<String, Set<Melon>> melonsFiltering = melons.stream() .collect(groupingBy(Melon::getType, filtering(m -> m.getWeight() > 2000, toSet())));
输出如下(每个Set
是一个容器):
{Crenshaw=[], Gac=[Gac(3000g)], Hemi=[Hemi(2600g)]}
请注意,没有比 2000g 重的 Crenshaw,因此filtering()
已将此类型映射到一个空集(容器)。现在,让我们通过filter()
重写这个:
Map<String, Set<Melon>> melonsFiltering = melons.stream() .filter(m -> m.getWeight() > 2000) .collect(groupingBy(Melon::getType, toSet()));
因为filter()
不会对其谓词失败的元素执行映射,所以输出将如下所示:
{Gac=[Gac(3000g)], Hemi=[Hemi(2600g)]}
用户问题:这次我只对哈密瓜感兴趣。有两个容器:一个用于装重量小于(或等于)2000 克的哈密瓜,另一个用于装重量大于 2000 克的哈密瓜。
过滤也可以与partitioningBy()
一起使用。要将重量超过 2000 克的西瓜进行分区,并按某种类型(在本例中为哈密瓜)进行过滤,我们有以下几点:
Map<Boolean, Set<Melon>> melonsFiltering = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000, filtering(m -> m.getType().equals("Hemi"), toSet())));
输出如下:
{false=[Hemi(1600g), Hemi(2000g)], true=[Hemi(2600g)]}
应用filter()
将导致相同的结果:
Map<Boolean, Set<Melon>> melonsFiltering = melons.stream() .filter(m -> m.getType().equals("Hemi")) .collect(partitioningBy(m -> m.getWeight() > 2000, toSet()));
输出如下:
{false=[Hemi(1600g), Hemi(2000g)], true=[Hemi(2600g)]}
mapping()
用户问题:对于每种类型的甜瓜,我都需要按升序排列的权重列表。
通过使用mapping(Function mapper, Collector downstream)
,我们可以对电流收集器的每个元件应用映射函数,并在下游收集器中累积输出。
例如,要按类型对西瓜的重量进行分组,我们可以编写以下代码片段:
Map<String, TreeSet<Integer>> melonsMapping = melons.stream() .collect(groupingBy(Melon::getType, mapping(Melon::getWeight, toCollection(TreeSet::new))));
输出如下:
{Crenshaw=[1700, 2000], Gac=[3000], Hemi=[1600, 2000, 2600]}
用户问题:我想要两个列表。一个应包含重量小于(或等于)2000 克的甜瓜类型,另一个应包含其余类型。
对重达 2000 克以上的西瓜进行分区,只收集其类型,可按以下步骤进行:
Map<Boolean, Set<String>> melonsMapping = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000, mapping(Melon::getType, toSet())));
输出如下:
{false=[Crenshaw, Hemi], true=[Gac, Hemi]}
flatMapping()
要快速提醒您如何展开流,建议阅读“映射”部分。
现在,假设我们有下面的Melon
列表(注意,我们还添加了有害生物的名称):
List<Melon> melonsGrown = Arrays.asList( new Melon("Honeydew", 5600, Arrays.asList("Spider Mites", "Melon Aphids", "Squash Bugs")), new Melon("Crenshaw", 2000, Arrays.asList("Pickleworms")), new Melon("Crenshaw", 1000, Arrays.asList("Cucumber Beetles", "Melon Aphids")), new Melon("Gac", 4000, Arrays.asList("Spider Mites", "Cucumber Beetles")), new Melon("Gac", 1000, Arrays.asList("Squash Bugs", "Squash Vine Borers")));
用户问题:对于每种类型的甜瓜,我想要一份它们的害虫清单。
所以,让我们把西瓜按种类分类,收集它们的害虫。每个甜瓜都没有、一个或多个害虫,因此我们预计产量为Map>
型。第一次尝试将依赖于mapping()
:
Map<String, List<List<String>>> pests = melonsGrown.stream() .collect(groupingBy(Melon::getType, mapping(m -> m.getPests(), toList())));
显然,这不是一个好方法,因为返回的类型是Map>>
。
另一种依赖于映射的简单方法如下:
Map<String, List<List<String>>> pests = melonsGrown.stream() .collect(groupingBy(Melon::getType, mapping(m -> m.getPests().stream(), toList())));
显然,这也不是一个好方法,因为返回的类型是Map>>
。
是时候介绍flatMapping()
了。通过使用flatMapping(Function> mapper, Collector downstream)
,我们将flatMapping
函数应用于电流收集器的每个元件,并在下游收集器中累积输出:
Map<String, Set<String>> pestsFlatMapping = melonsGrown.stream() .collect(groupingBy(Melon::getType, flatMapping(m -> m.getPests().stream(), toSet())));
这一次,类型看起来很好,输出如下:
{ Crenshaw = [Cucumber Beetles, Pickleworms, Melon Aphids], Gac = [Cucumber Beetles, Squash Bugs, Spider Mites, Squash Vine Borers], Honeydew = [Squash Bugs, Spider Mites, Melon Aphids] }
用户问题:我想要两个列表。一种应含有重量小于 2000 克的瓜类害虫,另一种应含有其余瓜类害虫。
对重达 2000 克以上的瓜类进行分区并收集害虫可按以下步骤进行:
Map<Boolean, Set<String>> pestsFlatMapping = melonsGrown.stream() .collect(partitioningBy(m -> m.getWeight() > 2000, flatMapping(m -> m.getPests().stream(), toSet())));
输出如下:
{ false = [Cucumber Beetles, Squash Bugs, Pickleworms, Melon Aphids, Squash Vine Borers], true = [Squash Bugs, Cucumber Beetles, Spider Mites, Melon Aphids] }
192 teeing()
从 JDK12 开始,我们可以通过Collectors.teeing()
合并两个收集器的结果:
public static Collector teeing(Collector downstream1, Collector downstream2, BiFunction merger)
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hglo8xPt-1657285412202)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/41ea4499-922d-4d62-bbe0-82f2bdd36311.png)]
结果是一个Collector
,它是两个经过下游收集器的组合。传递给结果收集器的每个元素都由两个下游收集器处理,然后使用指定的BiFunction
将它们的结果合并到最终结果中。
让我们看一个经典问题。下面的类仅存储整数流中的元素数及其和:
public class CountSum { private final Long count; private final Integer sum; public CountSum(Long count, Integer sum) { this.count = count; this.sum = sum; } ... }
我们可以通过teeing()
获得此信息,如下所示:
CountSum countsum = Stream.of(2, 11, 1, 5, 7, 8, 12) .collect(Collectors.teeing( counting(), summingInt(e -> e), CountSum::new));
这里,我们将两个收集器应用于流中的每个元素(counting()
和summingInt()
),结果已合并到CountSum
的实例中:
CountSum{count=7, sum=46}
让我们看看另一个问题。这次,MinMax
类存储整数流的最小值和最大值:
public class MinMax { private final Integer min; private final Integer max; public MinMax(Integer min, Integer max) { this.min = min; this.max = max; } ... }
现在,我们可以得到这样的信息:
MinMax minmax = Stream.of(2, 11, 1, 5, 7, 8, 12) .collect(Collectors.teeing( minBy(Comparator.naturalOrder()), maxBy(Comparator.naturalOrder()), (Optional<Integer> a, Optional<Integer> b) -> new MinMax(a.orElse(Integer.MIN_VALUE), b.orElse(Integer.MAX_VALUE))));
这里,我们将两个收集器应用于流中的每个元素(minBy()
和maxBy()
),结果已合并到MinMax
的实例中:
MinMax{min=1, max=12}
最后,考虑Melon
的Melon
类和List
:
public class Melon { private final String type; private final int weight; public Melon(String type, int weight) { this.type = type; this.weight = weight; } ... } List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 1200), new Melon("Gac", 3000), new Melon("Hemi", 2600), new Melon("Hemi", 1600), new Melon("Gac", 1200), new Melon("Apollo", 2600), new Melon("Horned", 1700), new Melon("Gac", 3000), new Melon("Hemi", 2600));
这里的目的是计算这些西瓜的总重量并列出它们的重量。我们可以将其映射如下:
public class WeightsAndTotal { private final int totalWeight; private final List<Integer> weights; public WeightsAndTotal(int totalWeight, List<Integer> weights) { this.totalWeight = totalWeight; this.weights = weights; } ... }
这个问题的解决依赖于Collectors.teeing()
,如下所示:
WeightsAndTotal weightsAndTotal = melons.stream() .collect(Collectors.teeing( summingInt(Melon::getWeight), mapping(m -> m.getWeight(), toList()), WeightsAndTotal::new));
这一次,我们应用了summingInt()
和mapping()
收集器。输出如下:
WeightsAndTotal { totalWeight = 19500, weights = [1200, 3000, 2600, 1600, 1200, 2600, 1700, 3000, 2600] }
193 编写自定义收集器
假设我们有以下Melon
类和Melon
的List
:
public class Melon { private final String type; private final int weight; private final List<String> grown; // constructors, getters, setters, equals(), // hashCode(), toString() omitted for brevity } List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 1200), new Melon("Gac", 3000), new Melon("Hemi", 2600), new Melon("Hemi", 1600), new Melon("Gac", 1200), new Melon("Apollo", 2600), new Melon("Horned", 1700), new Melon("Gac", 3000), new Melon("Hemi", 2600));
在“分割”部分,我们看到了如何使用partitioningBy()
收集器对重达 2000 克的西瓜进行分割:
Map<Boolean, List<Melon>> byWeight = melons.stream() .collect(partitioningBy(m -> m.getWeight() > 2000));
现在,让我们看看是否可以通过专用的定制收集器实现相同的结果。
首先,让我们说编写自定义收集器不是一项日常任务,但是知道如何做可能会很有用。内置 JavaCollector
接口如下:
public interface Collector<T, A, R> { Supplier<A> supplier(); BiConsumer<A, T> accumulator(); BinaryOperator<A> combiner(); Function<A, R> finisher(); Set<Characteristics> characteristics(); ... }
要编写自定义收集器,非常重要的一点是要知道,T
、A
和R
表示以下内容:
T
表示Stream
中的元素类型(将被收集的元素)。A
表示收集过程中使用的对象类型,称为累加器,用于将流元素累加到可变结果容器中。R
表示采集过程(最终结果)后的对象类型。
收集器可以返回累加器本身作为最终结果,或者可以对累加器执行可选转换以获得最终结果(执行从中间累加器类型A
到最终结果类型R
的可选最终转换)。
就我们的问题而言,我们知道T
是Melon
、A
是Map>
、R
是Map>
。此收集器通过Function.identity()
返回累加器本身作为最终结果。也就是说,我们可以按如下方式启动自定义收集器:
public class MelonCollector implements Collector<Melon, Map<Boolean, List<Melon>>, Map<Boolean, List<Melon>>> { ... }
因此,Collector
由四个函数指定。这些函数一起工作,将条目累积到可变的结果容器中,并可以选择对结果执行最终转换。具体如下:
- 新建空的可变结果容器(
supplier()
) - 将新的数据元素合并到可变结果容器中(
accumulator()
) - 将两个可变结果容器合并为一个(
combiner()
) - 对可变结果容器执行可选的最终转换以获得最终结果(
finisher()
)
此外,收集器的行为在最后一种方法characteristics()
中定义。Set
可以包含以下四个值:
UNORDERED
:元素积累/收集的顺序对最终结果并不重要。CONCURRENT
:流中的元素可以由多个线程并发地累加(最终收集器可以对流进行并行归约)。流的并行处理产生的容器组合在单个结果容器中。数据源的性质应该是无序的,或者应该有UNORDERED
标志。IDENTITY_FINISH
:表示累加器本身就是最终结果(基本上我们可以将A
强制转换为R
),此时不调用finisher()
。
供应者——Supplier supplier()
supplier()
的任务是(在每次调用时)返回一个空的可变结果容器的Supplier
。
在我们的例子中,结果容器是Map>
类型,因此supplier()
可以实现如下:
@Override public Supplier<Map<Boolean, List<Melon>>> supplier() { return () -> { return new HashMap<Boolean, List<Melon>> () { { put(true, new ArrayList<>()); put(false, new ArrayList<>()); } }; }; }
累积元素——BiConsumer accumulator()
accumulator()
方法返回执行归约操作的函数。这是BiConsumer
,这是一个接受两个输入参数但不返回结果的操作。第一个输入参数是当前结果容器(到目前为止是归约的结果),第二个输入参数是流中的当前元素。此函数通过累积遍历的元素或遍历此元素的效果来修改结果容器本身。在我们的例子中,accumulator()
将当前遍历的元素添加到两个ArrayList
之一:
@Override public BiConsumer<Map<Boolean, List<Melon>>, Melon> accumulator() { return (var acc, var melon) -> { acc.get(melon.getWeight() > 2000).add(melon); }; }
应用最终转换——Function finisher()
finisher()
方法返回在累积过程结束时应用的函数。调用此方法时,没有更多的流元素可遍历。所有元素将从中间累积类型A
累积到最终结果类型R
。如果不需要转换,那么我们可以返回中间结果(累加器本身):
@Override public Function<Map<Boolean, List<Melon>>, Map<Boolean, List<Melon>>> finisher() { return Function.identity(); }
并行化收集器——BinaryOperator combiner()
@Override public BinaryOperator<Map<Boolean, List<Melon>>> combiner() { return (var map, var addMap) -> { map.get(true).addAll(addMap.get(true)); map.get(false).addAll(addMap.get(false)); return map; }; }
返回最终结果–Function finisher()
最终结果用finisher()
方法计算。在这种情况下,我们只返回Function.identity()
,因为累加器不需要任何进一步的转换:
@Override public Function<Map<Boolean, List<Melon>>, Map<Boolean, List<Melon>>> finisher() { return Function.identity(); }
特征——Set characteristics()
最后,我们指出我们的收集器是IDENTITY_FINISH
和CONCURRENT
:
@Override public Set<Characteristics> characteristics() { return Set.of(IDENTITY_FINISH, CONCURRENT); }
本书附带的代码将拼图的所有部分粘在一个名为MelonCollector
的类中。
Java 编程问题:九、函数式编程——深入研究5https://developer.aliyun.com/article/1426158