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()
等原始类型提供口味。这些方法返回Stream
(IntStream
)的int
原初特化、Stream
(LongStream
)的long
原初特化和Stream
(StreamDouble
)的double
原初特化。
虽然map()
可以通过Function
将Stream
的元素映射到新的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()
可以产生诸如Stream
、Stream>
、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()
等原始类型提供口味。这些方法返回Stream
(IntStream
)的int
原特化、Stream
(LongStream
)的long
原特化和Stream
(StreamDouble
的double
原特化。
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()
)? - 最重的瓜是什么?
- 最轻的瓜是什么?
为了计算西瓜的总重量,我们需要把所有的重量加起来。对于Stream
(IntStream
、LongStream
等)的原始特化,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()
操作是如何工作的呢?
让我们看一下以下步骤来解决这个问题:
- 首先,0 被用作 Lambda 的第一个参数(
m1
),2000 被从流中消耗并用作第二个参数(m2)
。0+2000
产生 2000,这成为新的累计值。 - 然后,用累积值和流的下一个元素 1600 再次调用 Lambda,其产生新的累积值 3600。
- 向前看,Lambda 被再次调用,并使用累计值和下一个元素 3000 生成 6600。
- 如果我们再向前一步,Lambda 会被再次调用,并使用累计值和下一个元素 2000 生成 8600。
- 最后,用 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 }
我们还假设有Melon
的List
:
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));
通常,流管道以流中元素的摘要结束。换句话说,我们需要在数据结构中收集结果,例如List
、Set
或Map
(以及它们的同伴)。
为了完成这项任务,我们可以依靠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 }
我们还假设有Melon
的List
:
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
中的Stream
API。在这个类别中,我们还有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
类(使用type
和weight
以及Melon
的List
:
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));
JavaStream
API 将计数、总和、最小、平均和最大操作分组在术语摘要下。用于执行摘要操作的方法可以在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