Java 编程问题:九、函数式编程——深入研究4

简介: Java 编程问题:九、函数式编程——深入研究

Java 编程问题:九、函数式编程——深入研究3https://developer.aliyun.com/article/1426156

获取全部

有没有办法在一次幺正运算中获得计数、和、平均值、最小值和最大值?

是的,有!当我们需要两个或更多这样的操作时,我们可以依赖于Collectors.summarizingInt()summarizingLong()summarizingDouble()。这些方法将这些操作分别包装在IntSummaryStatisticsLongSummaryStatisticsDoubleSummaryStatistics中,如下所示:

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类和MelonList

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)
);

JavaStreamAPI 通过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都有一个类型、一个重量和一个糖分水平指示器。首先,我们要根据糖分指标(LOWMEDIUMHIGHUNKNOWN(默认值))对西瓜进行分组。此外,我们想把西瓜按重量分组。这可以通过两个级别的分组来实现,如下所示:

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类和MelonList

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类和MelonList

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));

JavaStreamAPI 提供了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}

最后,考虑MelonMelon类和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类和MelonList

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();
  ...
}

要编写自定义收集器,非常重要的一点是要知道,TAR表示以下内容:

  • T表示Stream中的元素类型(将被收集的元素)。
  • A表示收集过程中使用的对象类型,称为累加器,用于将流元素累加到可变结果容器中。
  • R表示采集过程(最终结果)后的对象类型。

收集器可以返回累加器本身作为最终结果,或者可以对累加器执行可选转换以获得最终结果(执行从中间累加器类型A到最终结果类型R的可选最终转换)。

就我们的问题而言,我们知道TMelonAMap>RMap>。此收集器通过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()

如果流是并行处理的,那么不同的线程(累加器)将生成部分结果容器。最后,这些部分结果必须合并成一个单独的结果。这正是combiner()所做的。在这种情况下,combiner()方法需要合并两个映射,将第二个Map的两个列表中的所有值加到第一个Map中相应的列表中:

@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_FINISHCONCURRENT

@Override
public Set<Characteristics> characteristics() {
  return Set.of(IDENTITY_FINISH, CONCURRENT);
}

本书附带的代码将拼图的所有部分粘在一个名为MelonCollector的类中。


Java 编程问题:九、函数式编程——深入研究5https://developer.aliyun.com/article/1426158

相关文章
|
3天前
|
存储 SQL 安全
Java 安全性编程:基本概念与实战指南
【4月更文挑战第27天】在当今的软件开发领域,安全性编程是一个至关重要的方面。Java,作为广泛使用的编程语言之一,提供了多种机制来保护应用免受常见的安全威胁。本博客将探讨 Java 安全性编程的基本概念,并通过实际示例来展示如何实现这些安全措施。
10 3
|
1天前
|
Java
Java中的条件语句结构在编程中的应用
Java中的条件语句结构在编程中的应用
4 0
|
1天前
|
安全 Java
Java修饰符在编程中的应用研究
Java修饰符在编程中的应用研究
6 0
|
1天前
|
Java 关系型数据库 MySQL
【JDBC编程】基于MySql的Java应用程序中访问数据库与交互数据的技术
【JDBC编程】基于MySql的Java应用程序中访问数据库与交互数据的技术
|
3天前
|
Java 开发者 UED
Java 异步和事件驱动编程:探索响应式模式
【4月更文挑战第27天】在现代软件开发中,异步和事件驱动编程是提高应用性能和响应性的关键策略。Java 提供了多种机制来支持这些编程模式,使开发者能够构建高效、可扩展的应用程序。
14 4
|
3天前
|
设计模式 Java
Java 设计模式:混合、装饰器与组合的编程实践
【4月更文挑战第27天】在面向对象编程中,混合(Mixins)、装饰器(Decorators)和组合(Composition)是三种强大的设计模式,用于增强和扩展类的功能。
9 1
|
3天前
|
Java
Java 事件驱动编程:概念、优势与实战示例
【4月更文挑战第27天】事件驱动编程是一种编程范式,其中程序的执行流程由外部事件的发生而触发或驱动。
9 0
|
3天前
|
Java Shell API
Java 模块化编程:概念、优势与实战指南
【4月更文挑战第27天】Java 模块化编程是 Java 9 中引入的一项重大特性,通过 Java Platform Module System (JPMS) 实现。模块化旨在解决 Java 应用的封装性、可维护性和性能问题
10 0
|
3天前
|
安全 Java
【JAVA】Java并发编程中的锁升级机制
【JAVA】Java并发编程中的锁升级机制
|
4天前
|
缓存 Java
Java并发编程:深入理解线程池
【4月更文挑战第26天】在Java中,线程池是一种重要的并发工具,它可以有效地管理和控制线程的执行。本文将深入探讨线程池的工作原理,以及如何使用Java的Executor框架来创建和管理线程池。我们将看到线程池如何提高性能,减少资源消耗,并提供更好的线程管理。