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

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

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

182 映射流的元素

映射一个流的元素是一个中间操作,用于将这些元素转换成一个新的版本,方法是将给定的函数应用于每个元素,并将结果累加到一个新的Stream(例如,将Stream转换成Stream,或将Stream转换成另一个Stream等)。

使用Stream.map()

基本上,我们调用Stream.map(Function mapper)对流的每个元素应用mapper函数。结果是一个新的Stream。不修改源Stream

假设我们有以下Melon类:

public class Melon {
  private String type;
  private int weight;
  // constructors, getters, setters, equals(),
  // hashCode(), toString() omitted for brevity
}

我们还需要假设我们有List

List<Melon> melons = Arrays.asList(new Melon("Gac", 2000),
  new Melon("Hemi", 1600), new Melon("Gac", 3000),
  new Melon("Apollo", 2000), new Melon("Horned", 1700));

此外,我们只想提取另一个列表中的瓜名,List

对于这个任务,我们可以依赖于map(),如下所示:

List<String> melonNames = melons.stream()
  .map(Melon::getType)
  .collect(Collectors.toList());

输出将包含以下类型的瓜:

Gac, Hemi, Gac, Apollo, Horned

下图描述了map()在本例中的工作方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sxpTU8sR-1657285412196)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/000bb488-d1bd-46e4-aa28-2778c8608518.png)]

因此,map()方法得到一个Stream,并输出一个Stream。每个Melon经过map()方法,该方法提取瓜的类型(即一个String),并存储在另一个Stream中。

同样,我们可以提取西瓜的重量。由于权重是整数,map()方法将返回一个Stream

List<Integer> melonWeights = melons.stream()
  .map(Melon::getWeight)
  .collect(Collectors.toList());

输出将包含以下权重:

2000, 1600, 3000, 2000, 1700

map()之外,Stream类还为mapToInt()mapToLong()mapToDouble()等原始类型提供口味。这些方法返回StreamIntStream)的int原初特化、StreamLongStream)的long原初特化和StreamStreamDouble)的double原初特化。

虽然map()可以通过FunctionStream的元素映射到新的Stream,但不能得出这样的结论:

List<Melon> lighterMelons = melons.stream()
  .map(m -> m.setWeight(m.getWeight() - 500))
  .collect(Collectors.toList());

这将无法工作/编译,因为setWeight()方法返回void。为了使它工作,我们需要返回Melon,但这意味着我们必须添加一些敷衍代码(例如,return):

List<Melon> lighterMelons = melons.stream()
  .map(m -> {
    m.setWeight(m.getWeight() - 500);
    return m;
  })
  .collect(Collectors.toList());

你觉得诱惑怎么样?好吧,peek()代表看但不要碰,但它可以用来改变状态,如下所示:

List<Melon> lighterMelons = melons.stream()
  .peek(m -> m.setWeight(m.getWeight() - 500))
  .collect(Collectors.toList());

输出将包含以下西瓜(这看起来很好):

Gac(1500g), Hemi(1100g), Gac(2500g), Apollo(1500g), Horned(1200g)

这比使用map()更清楚。调用setWeight()是一个明确的信号,表明我们计划改变状态,但是文档中指定传递给peek()Consumer应该是一个非干扰动作(不修改流的数据源)。

对于连续流(如前一个流),打破这一预期可以得到控制,而不会产生副作用;然而,对于并行流管道,问题可能会变得更复杂。

可以在上游操作使元素可用的任何时间和线程中调用该操作,因此如果该操作修改共享状态,它将负责提供所需的同步。

根据经验,在使用peek()改变状态之前,要三思而后行。另外,请注意,这种做法是一种辩论,属于不良做法,甚至反模式的保护伞。

使用Stream.flatMap()

正如我们刚才看到的,map()知道如何在Stream中包装一系列元素。

这意味着map()可以产生诸如StreamStream>Stream>甚至Stream>的流。

但问题是,这些类型的流不能被成功地操作(或者,正如我们所预期的那样),比如流操作,比如sum()distinct()filter()等等。

例如,让我们考虑下面的Melon数组:

Melon[][] melonsArray = {
  {new Melon("Gac", 2000), new Melon("Hemi", 1600)}, 
  {new Melon("Gac", 2000), new Melon("Apollo", 2000)}, 
  {new Melon("Horned", 1700), new Melon("Hemi", 1600)}
};

我们可以通过Arrays.stream()将这个数组包装成一个流,如下代码片段所示:

Stream<Melon[]> streamOfMelonsArray = Arrays.stream(melonsArray);

有许多其他方法可以获得数组的Stream。例如,如果我们有一个字符串,s,那么map(s -> s.split(""))将返回一个Stream

现在,我们可以认为,获得不同的Melon实例就足够调用distinct(),如下所示:

streamOfMelonsArray
  .distinct()
  .collect(Collectors.toList());

但这是行不通的,因为distinct()不会寻找一个不同的Melon;相反,它会寻找一个不同的数组Melon[],因为这是我们在流中拥有的。

此外,在本例中返回的结果是Stream类型,而不是Stream类型。最终结果将在List中收集Stream

我们怎样才能解决这个问题?

我们可以考虑应用Arrays.stream()Melon[]转换为Stream

streamOfMelonsArray
  .map(Arrays::stream) // Stream<Stream<Melon>>
  .distinct()
  .collect(Collectors.toList());

再说一遍,map()不会做我们认为它会做的事。

首先,调用Arrays.stream()将从每个给定的Melon[]返回一个Stream。但是,map()返回元素的Stream,因此它将把应用Arrays.stream()的结果包装成Stream。它将在Stream>中结束。

所以,这一次,distinct()试图检测不同的Stream元素:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qjB2quEe-1657285412197)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/368e3557-d155-4aae-bba2-c26bc0a960be.png)]

为了解决这个问题,我们必须依赖于flatMap(),下图描述了flatMap()是如何在内部工作的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O5sSw8sA-1657285412198)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/a59fe93d-cdd6-42f0-afc9-767558fcab8f.png)]

map()不同,该方法通过展开所有分离的流来返回流。因此,所有数组都将在同一个流中结束:

streamOfMelonsArray
  .flatMap(Arrays::stream) // Stream<Melon>
  .distinct()
  .collect(Collectors.toList());

根据Melon.equals()实现,输出将包含不同的瓜:

Gac(2000g), Hemi(1600g), Apollo(2000g), Horned(1700g)

现在,让我们尝试另一个问题,从一个List>开始,如下所示:

List<List<String>> melonLists = Arrays.asList(
  Arrays.asList("Gac", "Cantaloupe"),
  Arrays.asList("Hemi", "Gac", "Apollo"),
  Arrays.asList("Gac", "Hemi", "Cantaloupe"),
  Arrays.asList("Apollo"),
  Arrays.asList("Horned", "Hemi"),
  Arrays.asList("Hemi"));

我们试图从这张单子上找出不同的瓜名。如果可以通过Arrays.stream()将数组包装成流,那么对于集合,我们有Collection.stream()。因此,第一次尝试可能如下所示:

melonLists.stream()
  .map(Collection::stream)
  .distinct();

但是基于前面的问题,我们已经知道这将不起作用,因为map()将返回Stream>

flatMap()提供的解决方案如下:

List<String> distinctNames = melonLists.stream()
  .flatMap(Collection::stream)
  .distinct()
  .collect(Collectors.toList());

输出如下:

Gac, Cantaloupe, Hemi, Apollo, Horned

flatMap()之外,Stream类还为flatMapToInt()flatMapToLong()flatMapToDouble()等原始类型提供口味。这些方法返回StreamIntStream)的int原特化、StreamLongStream)的long原特化和StreamStreamDoubledouble原特化。

183 在流中查找元素

除了使用filter()允许我们通过谓词过滤流中的元素外,我们还可以通过anyFirst()findFirst()在流中找到元素。

假设我们将以下列表包装在流中:

List<String> melons = Arrays.asList(
  "Gac", "Cantaloupe", "Hemi", "Gac", "Gac", 
    "Hemi", "Cantaloupe", "Horned", "Hemi", "Hemi");

findAny()

findAny()方法从流中返回任意(不确定)元素。例如,以下代码片段将返回前面列表中的元素:

Optional<String> anyMelon = melons.stream()
  .findAny();
if (!anyMelon.isEmpty()) {
  System.out.println("Any melon: " + anyMelon.get());
} else {
  System.out.println("No melon was found");
}

注意,不能保证每次执行时都返回相同的元素。这种说法是正确的,尤其是在并行流的情况下。

我们也可以将findAny()与其他操作结合起来。举个例子:

String anyApollo = melons.stream()
  .filter(m -> m.equals("Apollo"))
  .findAny()
  .orElse("nope");

这一次,结果将是nope。列表中没有Apollo,因此filter()操作将产生一个空流。此外,findAny()还将返回一个空流,因此orElse()将返回最终结果作为指定的字符串nope

findFirst()

如果findAny()返回任何元素,findFirst()返回流中的第一个元素。显然,当我们只对流的第一个元素感兴趣时(例如,竞赛的获胜者应该是竞争对手排序列表中的第一个元素),这种方法很有用。

然而,如果流没有相遇顺序,则可以返回任何元素。根据文档,流可能有也可能没有定义的相遇顺序。这取决于源和中间操作。同样的规则也适用于并行性。

现在,假设我们想要列表中的第一个瓜:

Optional<String> firstMelon = melons.stream()
  .findFirst();
if (!firstMelon.isEmpty()) {
  System.out.println("First melon: " + firstMelon.get());
} else {
  System.out.println("No melon was found");
}

输出如下:

First melon: Gac

我们也可以将findFirst()与其他操作结合起来。举个例子:

String firstApollo = melons.stream()
  .filter(m -> m.equals("Apollo"))
  .findFirst()
  .orElse("nope");

这一次,结果将是nope,因为filter()将产生一个空流。

下面是整数的另一个问题(只需按照右侧的注释快速发现流):

List<Integer> ints = Arrays.asList(4, 8, 4, 5, 5, 7);
int result = ints.stream()
  .map(x -> x * x - 1)     // 23, 63, 23, 24, 24, 48
  .filter(x -> x % 2 == 0) // 24, 24, 48
  .findFirst()             // 24
  .orElse(-1);

184 匹配流中的元素

为了匹配Stream中的某些元素,我们可以采用以下方法:

  • anyMatch()
  • noneMatch()
  • allMatch()

所有这些方法都以一个Predicate作为参数,并针对它获取一个boolean结果。

这三种操作依赖于短路技术。换句话说,在我们处理整个流之前,这些方法可能会返回。例如,如果allMatch()匹配false(将给定的Predicate求值为false,则没有理由继续。最终结果为false

假设我们将以下列表包装在流中:

List<String> melons = Arrays.asList(
  "Gac", "Cantaloupe", "Hemi", "Gac", "Gac", "Hemi", 
    "Cantaloupe", "Horned", "Hemi", "Hemi");

现在,让我们试着回答以下问题:

  • 元素是否与Gac字符串匹配?让我们看看下面的代码:
boolean isAnyGac = melons.stream()
  .anyMatch(m -> m.equals("Gac")); // true
  • 元素是否与Apollo字符串匹配?让我们看看下面的代码:
boolean isAnyApollo = melons.stream()
  .anyMatch(m -> m.equals("Apollo")); // false

作为一般性问题,流中是否有与给定谓词匹配的元素?

  • 没有与Gac字符串匹配的元素吗?让我们看看下面的代码:
boolean isNoneGac = melons.stream()
  .noneMatch(m -> m.equals("Gac")); // false
  • 没有与Apollo字符串匹配的元素吗?让我们看看下面的代码:
boolean isNoneApollo = melons.stream()
  .noneMatch(m -> m.equals("Apollo")); // true

一般来说,流中没有与给定谓词匹配的元素吗?

  • 所有元素都与Gac字符串匹配吗?让我们看看下面的代码:
boolean areAllGac = melons.stream()
  .allMatch(m -> m.equals("Gac")); // false
  • 所有元素都大于 2 吗?让我们看看下面的代码:
boolean areAllLargerThan2 = melons.stream()
  .allMatch(m -> m.length() > 2);

作为一般问题,流中的所有元素是否都与给定的谓词匹配?

185 流中的总和,最大和最小

假设我们有以下Melon类:

public class Melon {
  private String type;
  private int weight;
  // constructors, getters, setters, equals(),
  // hashCode(), toString() omitted for brevity
}

我们还假设在一个流中包装了下面的Melon列表:

List<Melon> melons = Arrays.asList(new Melon("Gac", 2000),
  new Melon("Hemi", 1600), new Melon("Gac", 3000),
  new Melon("Apollo", 2000), new Melon("Horned", 1700));

让我们使用sum()min()max()终端操作来处理Melon类。

sum()min()max()终端操作

现在,让我们结合此流的元素来表示以下查询:

  • 如何计算瓜的总重量(sum())?
  • 最重的瓜是什么?
  • 最轻的瓜是什么?

为了计算西瓜的总重量,我们需要把所有的重量加起来。对于StreamIntStreamLongStream等)的原始特化,Java 流 API 公开了一个名为sum()的终端操作。顾名思义,这个方法总结了流的元素:

int total = melons.stream()
  .mapToInt(Melon::getWeight)
  .sum();

sum()之后,我们还有max()min()终端操作。显然,max()返回流的最大值,min()则相反:

int max = melons.stream()
  .mapToInt(Melon::getWeight)
  .max()
  .orElse(-1);
int min = melons.stream()
  .mapToInt(Melon::getWeight)
  .min()
  .orElse(-1);

max()min()操作返回一个OptionalInt(例如OptionalLong)。如果无法计算最大值或最小值(例如,在空流的情况下),则我们选择返回-1。既然我们是在处理权值,以及正数的性质,返回-1是有意义的。但不要把这当成一个规则。根据情况,应该返回另一个值,或者使用orElseGet()/orElseThrow()更好。

对于非原始特化,请查看本章的“摘要收集器”部分。

让我们在下一节学习如何减少。

归约

sum()max()min()被称为归约的特例。我们所说的归约,是指基于两个主要语句的抽象:

  • 取初始值(T
  • 取一个BinaryOperator将两个元素结合起来,产生一个新的值

缩减可以通过名为reduce()的终端操作来完成,该操作遵循此抽象并定义两个签名(第二个签名不使用初始值):

  • T reduce(T identity, BinaryOperator accumulator)
  • Optional reduce(BinaryOperator accumulator)

也就是说,我们可以依赖于reduce()终端运算来计算元素的和,如下所示(初始值为 0,λ为(m1, m2) -> m1 + m2)):

int total = melons.stream()
  .map(Melon::getWeight)
  .reduce(0, (m1, m2) -> m1 + m2);

下图描述了reduce()操作的工作原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uumt5UhR-1657285412198)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/e68d3bbb-1916-4c06-87ef-47fcca63edda.png)]

那么,reduce()操作是如何工作的呢?

让我们看一下以下步骤来解决这个问题:

  1. 首先,0 被用作 Lambda 的第一个参数(m1),2000 被从流中消耗并用作第二个参数(m2)0+2000产生 2000,这成为新的累计值。
  2. 然后,用累积值和流的下一个元素 1600 再次调用 Lambda,其产生新的累积值 3600。
  3. 向前看,Lambda 被再次调用,并使用累计值和下一个元素 3000 生成 6600。
  4. 如果我们再向前一步,Lambda 会被再次调用,并使用累计值和下一个元素 2000 生成 8600。
  5. 最后,用 8600 调用 Lambda,流的最后一个元素 1700 产生最终值 10300。

也可以计算最大值和最小值:

int max = melons.stream()
  .map(Melon::getWeight)
  .reduce(Integer::max)
  .orElse(-1);
int min = melons.stream()
  .map(Melon::getWeight)
  .reduce(Integer::min)
  .orElse(-1);

使用reduce()的优点是,我们可以通过简单地传递另一个 Lambda 来更改计算。例如,我们可以快速地用乘积替换总和,如下例所示:

List<Double> numbers = Arrays.asList(1.0d, 5.0d, 8.0d, 10.0d);
double total = numbers.stream()
  .reduce(1.0 d, (x1, x2) -> x1 * x2);

然而,要注意那些可能导致不想要的结果的案例。例如,如果我们要计算给定数字的调和平均数,那么就没有一个开箱即用的归约特例,因此我们只能依赖reduce(),如下所示:

List<Double> numbers = Arrays.asList(1.0d, 5.0d, 8.0d, 10.0d);

调和平均公式如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rb0BfYEI-1657285412199)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/d8c10384-922c-4c02-b3b5-663a638b9d71.png)]

在我们的例子中,n是列表的大小,H是 2.80701。使用简单的reduce()函数将如下所示:

double hm = numbers.size() / numbers.stream()
  .reduce((x1, x2) -> (1.0d / x1 + 1.0d / x2))
  .orElseThrow();

这将产生 3.49809。

这个解释依赖于我们如何表达计算。在第一步中,我们计算1.0/1.0+1.0/5.0=1.2。那么,我们可以期望做1.2+1.0/1.8,但实际上,计算是1.0/1.2+1.0/1.8。显然,这不是我们想要的。

我们可以使用mapToDouble()来解决这个问题,如下所示:

double hm = numbers.size() / numbers.stream()
  .mapToDouble(x -> 1.0d / x)
  .reduce((x1, x2) -> (x1 + x2))
  .orElseThrow();

这将产生预期结果,即 2.80701。

186 收集流的结果

假设我们有以下Melon类:

public class Melon {
  private String type;
  private int weight;
  // constructors, getters, setters, equals(),
  // hashCode(), toString() omitted for brevity
}

我们还假设有MelonList

List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 2000),
  new Melon("Hemi", 1600), new Melon("Gac", 3000),
  new Melon("Apollo", 2000), new Melon("Horned", 1700),
  new Melon("Gac", 3000), new Melon("Cantaloupe", 2600));

通常,流管道以流中元素的摘要结束。换句话说,我们需要在数据结构中收集结果,例如ListSetMap(以及它们的同伴)。

为了完成这项任务,我们可以依靠Stream.collect(Collector collector)方法。此方法获取一个表示java.util.stream.Collector或用户定义的Collector的参数。

最著名的收集器包括:

  • toList()
  • toSet()
  • toMap()
  • toCollection()

他们的名字不言自明。我们来看几个例子:

  • 过滤重量超过 1000g 的瓜,通过toList()toCollection()将结果收集到List中:
List<Integer> resultToList = melons.stream()
  .map(Melon::getWeight)
  .filter(x -> x >= 1000)
  .collect(Collectors.toList());
List<Integer> resultToList = melons.stream()
  .map(Melon::getWeight)
  .filter(x -> x >= 1000)
  .collect(Collectors.toCollection(ArrayList::new));

toCollection()方法的参数为Supplier,提供了一个新的空Collection,结果将插入其中。

  • 过滤重量超过 1000g 的瓜,通过toSet()toCollection()Set中收集无重复的结果:
Set<Integer> resultToSet = melons.stream()
  .map(Melon::getWeight)
  .filter(x -> x >= 1000)
  .collect(Collectors.toSet());
Set<Integer> resultToSet = melons.stream()
  .map(Melon::getWeight)
  .filter(x -> x >= 1000)
  .collect(Collectors.toCollection(HashSet::new));
  • 过滤重量超过 1000 克的瓜,收集无重复的结果,通过toCollection()Set升序排序:
Set<Integer> resultToSet = melons.stream()
  .map(Melon::getWeight)
  .filter(x -> x >= 1000)
  .collect(Collectors.toCollection(TreeSet::new));
  • 过滤不同的Melon,通过toMap()将结果收集到Map中:
Map<String, Integer> resultToMap = melons.stream()
  .distinct()
  .collect(Collectors.toMap(Melon::getType, 
    Melon::getWeight));

toMap()方法的两个参数表示一个映射函数,用于生成键及其各自的值(如果两个Melon具有相同的键,则容易出现java.lang.IllegalStateException重复键异常)。

  • 过滤一个不同的Melon并使用随机键通过toMap()将结果收集到Map中(如果生成两个相同的键,则容易产生java.lang.IllegalStateException重复键):
Map<Integer, Integer> resultToMap = melons.stream()
  .distinct()
  .map(x -> Map.entry(
    new Random().nextInt(Integer.MAX_VALUE), x.getWeight()))
  .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
  • 通过toMap()采集映射中的Melon,并通过选择现有(旧)值避免可能的java.lang.IllegalStateException重复键,以防发生键冲突:
Map<String, Integer> resultToMap = melons.stream()
  .collect(Collectors.toMap(Melon::getType, Melon::getWeight,
    (oldValue, newValue) -> oldValue));

toMap()方法的最后一个参数是一个merge函数,用于解决提供给Map.merge(Object, Object, BiFunction)的与同一个键相关的值之间的冲突。

显然,可以通过(oldValue, newValue) -> newValue选择新值:

  • 将上述示例放入排序后的Map(例如,按重量):
Map<String, Integer> resultToMap = melons.stream()
 .sorted(Comparator.comparingInt(Melon::getWeight))
 .collect(Collectors.toMap(Melon::getType, Melon::getWeight,
   (oldValue, newValue) -> oldValue,
     LinkedHashMap::new));

这个toMap()风格的最后一个参数表示一个Supplier,它提供了一个新的空Map,结果将被插入其中。在本例中,需要这个Supplier来保存排序后的顺序。因为HashMap不能保证插入的顺序,所以我们需要依赖LinkedHashMap

  • 通过toMap()采集词频计数:
String str = "Lorem Ipsum is simply 
              Ipsum Lorem not simply Ipsum";
Map<String, Integer> mapOfWords = Stream.of(str)
  .map(w -> w.split("\\s+"))
  .flatMap(Arrays::stream)
  .collect(Collectors.toMap(
    w -> w.toLowerCase(), w -> 1, Integer::sum));

除了toList()toMap()toSet()之外,Collectors类还将收集器公开给不可修改的并发集合,例如toUnmodifiableList()toConcurrentMap()等等。

187 连接流的结果

假设我们有以下Melon类:

public class Melon {
  private String type;
  private int weight;
  // constructors, getters, setters, equals(),
  // hashCode(), toString() omitted for brevity
}

我们还假设有MelonList

List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 2000),
  new Melon("Hemi", 1600), new Melon("Gac", 3000),
  new Melon("Apollo", 2000), new Melon("Horned", 1700),
  new Melon("Gac", 3000), new Melon("Cantaloupe", 2600));

在上一个问题中,我们讨论了内置于Collectors中的StreamAPI。在这个类别中,我们还有Collectors.joining()。这些收集器的目标是将流中的元素连接成一个按相遇顺序String。或者,这些收集器可以使用分隔符、前缀和后缀,因此最全面的joining()风格是String joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix)

但是,如果我们只想在不使用分隔符的情况下连接西瓜的名称,那么这就是一种方法(只是为了好玩,让我们排序并删除重复的名称):

String melonNames = melons.stream()
  .map(Melon::getType)
  .distinct()
  .sorted()
  .collect(Collectors.joining());

我们将收到以下输出:

ApolloCantaloupeCrenshawGacHemiHorned

更好的解决方案是添加分隔符,例如逗号和空格:

String melonNames = melons.stream()
  ...
  .collect(Collectors.joining(", "));

我们将收到以下输出:

Apollo, Cantaloupe, Crenshaw, Gac, Hemi, Horned

我们还可以使用前缀和后缀来丰富输出:

String melonNames = melons.stream()
  ...
  .collect(Collectors.joining(", ", 
    "Available melons: ", " Thank you!"));

我们将收到以下输出:

Available melons: Apollo, Cantaloupe, Crenshaw, Gac, Hemi, Horned Thank you!

188 摘要收集器

假设我们有著名的Melon类(使用typeweight以及MelonList

List<Melon> melons = Arrays.asList(new Melon("Crenshaw", 2000),
  new Melon("Hemi", 1600), new Melon("Gac", 3000),
  new Melon("Apollo", 2000), new Melon("Horned", 1700),
  new Melon("Gac", 3000), new Melon("Cantaloupe", 2600));

JavaStreamAPI 将计数、总和、最小、平均和最大操作分组在术语摘要下。用于执行摘要操作的方法可以在Collectors类中找到。

我们将在下面的部分中查看所有这些操作。

求和

假设我们要把所有的西瓜重量加起来。我们通过Stream的原始特化在流部分的“总和最小和最大”中实现了这一点。现在,让我们通过summingInt(ToIntFunction mapper)收集器来完成:

int sumWeightsGrams = melons.stream()
  .collect(Collectors.summingInt(Melon::getWeight));

所以,Collectors.summingInt()是一个工厂方法,它接受一个函数,这个函数能够将一个对象映射成一个int,这个函数必须作为一个参数求和。返回一个收集器,该收集器通过collect()方法执行摘要。下图描述了summingInt()的工作原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l0pEzQsD-1657285412199)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/29b1ee07-f595-4c82-821a-af4776bf2edc.png)]

当遍历流时,每个权重(Melon::getWeight)被映射到它的数字,并且这个数字被添加到累加器中,从初始值开始,即 0。

summingInt()之后,我们有summingLong()summingDouble()。我们怎样用公斤来计算西瓜的重量?这可以通过summingDouble()实现,如下所示:

double sumWeightsKg = melons.stream()
  .collect(Collectors.summingDouble(
    m -> (double) m.getWeight() / 1000.0d));

如果我们只需要以千克为单位的结果,我们仍然可以以克为单位求和,如下所示:

double sumWeightsKg = melons.stream()
  .collect(Collectors.summingInt(Melon::getWeight)) / 1000.0d;

因为摘要实际上是归约,所以Collectors类也提供了reducing()方法。显然,这种方法有更广泛的用途,允许我们通过其三种口味提供各种 Lambda:

  • reducing(BinaryOperator op)
  • reducing(T identity, BinaryOperator op)
  • reducing(U identity, Function mapper, BinaryOperator op)

reducing()的参数很直截了当。我们有用于减少的identity值(以及没有输入元素时返回的值)、应用于每个输入值的映射函数和用于减少映射值的函数。

例如,让我们通过reducing()重写前面的代码片段。请注意,我们从 0 开始求和,通过映射函数将其从克转换为千克,并通过 Lambda 减少值(结果千克):

double sumWeightsKg = melons.stream()
  .collect(Collectors.reducing(0.0,
    m -> (double) m.getWeight() / 1000.0d, (m1, m2) -> m1 + m2));

或者,我们可以简单地在末尾转换为千克:

double sumWeightsKg = melons.stream()
  .collect(Collectors.reducing(0,
    m -> m.getWeight(), (m1, m2) -> m1 + m2)) / 1000.0d;

当没有合适的内置解决方案时,依赖reducing()。把reducing()想象成摘要

平均

计算一个瓜的平均重量怎么样?

为此,我们有Collectors.averagingInt()averagingLong()averagingDouble()

double avgWeights = melons.stream()
  .collect(Collectors.averagingInt(Melon::getWeight));

计数

计算一段文字的字数是一个常见的问题,可以通过count()来解决:

String str = "Lorem Ipsum is simply dummy text ...";
long numberOfWords = Stream.of(str)
  .map(w -> w.split("\\s+"))
  .flatMap(Arrays::stream)
  .filter(w -> w.trim().length() != 0)
  .count();

但是让我们看看我们的流里有多少重 3000 磅的Melon

long nrOfMelon = melons.stream()
  .filter(m -> m.getWeight() == 3000)
  .count();

我们可以使用counting()工厂方法返回的收集器:

long nrOfMelon = melons.stream()
  .filter(m -> m.getWeight() == 3000)
  .collect(Collectors.counting());

我们也可以使用笨拙的方法使用reducing()

long nrOfMelon = melons.stream()
  .filter(m -> m.getWeight() == 3000)
  .collect(Collectors.reducing(0L, m -> 1L, Long::sum));

最大值和最小值

在“流的求和、最大、最小”部分,我们已经通过min()max()方法计算了最小值和最大值。这次,让我们通过Collectors.maxBy()Collectors.minBy()收集器来计算最重和最轻的Melon。这些收集器以一个Comparator作为参数来比较流中的元素,并返回一个Optional(如果流为空,则该Optional将为空):

Comparator<Melon> byWeight = Comparator.comparing(Melon::getWeight);
Melon heaviestMelon = melons.stream()
  .collect(Collectors.maxBy(byWeight))
  .orElseThrow();
Melon lightestMelon = melons.stream()
  .collect(Collectors.minBy(byWeight))
  .orElseThrow();

在这种情况下,如果流是空的,我们只抛出NoSuchElementException


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

相关文章
|
7天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
21 2
|
8天前
|
并行计算 Java 测试技术
探索Java中的函数式编程
在本文中,我们将深入探讨Java中的函数式编程。我们会先了解什么是函数式编程以及为什么它如此重要。然后,通过一些简单的代码示例,展示如何在Java中应用函数式编程概念。最后,讨论在实际项目中如何利用函数式编程来提高代码的可读性和效率。
|
9天前
|
Java API 开发者
探索Java中的函数式编程
本文深入探讨了Java中的函数式编程,这是一种强调使用不可变数据和避免共享状态的编程范式。我们将从基础概念、核心特性以及实际应用案例三个方面,全面解析函数式编程在Java中的魅力和价值。
|
8天前
|
Java C语言
5-13|Java的函数式编程
5-13|Java的函数式编程
|
11天前
|
算法 安全 Java
JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏
很多人都以为TreadLocal很难很深奥,尤其被问到ThreadLocal数据结构、以及如何发生的内存泄漏问题,候选人容易谈虎色变。 日常大家用这个的很少,甚至很多近10年资深研发人员,都没有用过ThreadLocal。本文由浅入深、并且才有通俗易懂方式全面分析ThreadLocal的应用场景、数据结构、内存泄漏问题。降低大家学习啃骨头的心理压力,希望可以帮助大家彻底掌握并应用这个核心技术到工作当中。
|
11天前
|
Java 程序员 编译器
死磕-高效的Java编程(二)
死磕-高效的Java编程(二)
|
5天前
|
Java
JAVA并发编程系列(13)Future、FutureTask异步小王子
本文详细解析了Future及其相关类FutureTask的工作原理与应用场景。首先介绍了Future的基本概念和接口方法,强调其异步计算特性。接着通过FutureTask实现了一个模拟外卖订单处理的示例,展示了如何并发查询外卖信息并汇总结果。最后深入分析了FutureTask的源码,包括其内部状态转换机制及关键方法的实现原理。通过本文,读者可以全面理解Future在并发编程中的作用及其实现细节。
|
9天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
9天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
11天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
下一篇
无影云桌面