原文:Java Coding Problems
协议:CC BY-NC-SA 4.0
贡献者:飞龙
本文来自【ApacheCN Java 谷歌翻译用谷歌翻译。
本章包括 22 个涉及 Java 函数编程的问题。在这里,我们将重点讨论一些涉及流程中经典操作的问题(例如,filter
和map
),并讨论无限流、空安全流和缺省方法。这个问题的综合列表将涵盖分组、分区和收集器,包括 JDK12teeing()
收集器和自定义收集器的编写。此外,还将讨论takeWhile()
、dropWhile()
、组合函数,谓词和比较器,Lambda 测试和调试以及其他很酷的话题。
一旦您涵盖了本章和前一章,您可以在生产应用程序中释放函数编程。以下问题将为您准备各种用例,包括角落用例或陷阱。
问题
使用以下问题来测试您的函数编程能力。在使用解决方案和下载示例程序之前,我强烈建议您尝试每个问题:
- :编写几个单元测试来测试所谓的高级函数。
- :为使用 Lambda 编写几个单元测试的测试方法。
- :提供调试 Lambda 的技术。
- :编写流管,过滤流中的非零元素。
- :编写几个处理无限流的代码片段。另外,写几个使用
takeWhile()
和dropWhile()
API 的例子。 - :写几个通过
map()
和flatMap()
映射流的例子。 - :在搜索流中编写不同元素的程序。
- :编写匹配流中不同元素的程序。
- :通过
Stream
和Stream.reduce()
编写和计算给定流的总和、最大和最小程序的原始类型。 - :编写一些代码片段,用于收集列表、映射和集合中的流的结果。
- :写几个代码片段,连接流结果
String
中。 - :写几个代码片段来显示摘要收集器的用法。
- :编写用于处理
groupingBy()
收集器的代码片段。 - :编写几个代码片段,用于使用
partitioningBy()
收集器。 - :编写几段代码,例如过滤、扩展和映射收集器的使用。
- :编写几个合并两个收集器(JDK12 和
Collectors.teeing()
示例结果。 - :编写一个表示自定义收集器的程序。
- :写一个方法引用的例子。
- :并行处理简介流。
parallelStream()
、parallel()
和spliterator()
至少提供一个示例。 - :编写一个从元素或元素集合回到空安全流的程序。
- :写几个组合函数、谓词和比较器的例子。
- :写一个包
default
接口的方法。
以下部分介绍了上述问题的解决方案。请记住,通常没有正确的方法来解决特定的问题。此外,请记住,这里显示的解释只包括解决问题所需的最有趣和最重要的细节。您可以从下载示例解决方案中查看更多详细信息并尝试程序。
177 测试高级函数
高阶函数用于描述返回函数或以函数为参数的术语。
基于这句话,在 Lambda 高级函数的测试应包括两种主要情况:
- 测试以 Lambda 作为参数的方法
- 测试返回函数接口的方法
这两个测试一部分了解这两个测试。
测试以 Lambda 作为参数的方法
将 Lambda 作为参数的方法的测试可以通过向该方法传递不同的 Lambda 例如,假设我们有以下函数接口:
@FunctionalInterface public interface Replacer<String> {
String replace(String s); }
我们还假设我们有一种接受它的方法String -> String
类型的 Lambda,如下所示:
public static List<String> replace( List<String> list, Replacer<String> r) {
List
<
String
> result
=
new
ArrayList
<>
(
)
;
for
(
String s
: list
)
{
result
.
add
(r
.
replace
(s
)
)
;
}
return result
;
}
现在,让我们使用两个 Lambda 为这个方法编写一个 JUnit 测试:
@Test
public void testReplacer() throws Exception {
List<String> names = Arrays.asList(
"Ann a 15", "Mir el 28", "D oru 33");
List<String> resultWs = replace(
names, (String s) -> s.replaceAll("\\s", ""));
List<String> resultNr = replace(
names, (String s) -> s.replaceAll("\\d", ""));
assertEquals(Arrays.asList(
"Anna15", "Mirel28", "Doru33"), resultWs);
assertEquals(Arrays.asList(
"Ann a ", "Mir el ", "D oru "), resultNr);
}
测试返回函数式接口的方法
另一方面,测试返回函数式接口的方法可以解释为测试该函数式接口的行为。让我们考虑以下方法:
public static Function<String, String> reduceStrings(
Function<String, String> ...functions) {
Function<String, String> function = Stream.of(functions)
.reduce(Function.identity(), Function::andThen);
return function;
}
现在,我们可以测试返回的Function<String, String>
的行为,如下所示:
@Test
public void testReduceStrings() throws Exception {
Function<String, String> f1 = (String s) -> s.toUpperCase();
Function<String, String> f2 = (String s) -> s.concat(" DONE");
Function<String, String> f = reduceStrings(f1, f2);
assertEquals("TEST DONE", f.apply("test"));
}
178 测试使用 Lambda 的方法
让我们从测试一个没有包装在方法中的 Lambda 开始。例如,以下 Lambda 与一个字段关联(用于重用),我们要测试其逻辑:
public static final Function<String, String> firstAndLastChar
= (String s) -> String.valueOf(s.charAt(0))
+ String.valueOf(s.charAt(s.length() - 1));
让我们考虑到 Lambda 生成函数式接口实例;然后,我们可以测试该实例的行为,如下所示:
@Test
public void testFirstAndLastChar() throws Exception {
String text = "Lambda";
String result = firstAndLastChar.apply(text);
assertEquals("La", result);
}
另一种解决方案是将 Lambda 包装在方法调用中,并为方法调用编写单元测试。
通常,Lambda 用于方法内部。对于大多数情况,测试包含 Lambda 的方法是可以接受的,但是在有些情况下,我们需要测试 Lambda 本身。这个问题的解决方案包括三个主要步骤:
- 用
static
方法提取 Lambda - 用方法引用替换 Lambda
- 测试这个
static
方法
例如,让我们考虑以下方法:
public List<String> rndStringFromStrings(List<String> strs) {
return strs.stream()
.map(str -> {
Random rnd = new Random();
int nr = rnd.nextInt(str.length());
String ch = String.valueOf(str.charAt(nr));
return ch;
})
.collect(Collectors.toList());
}
我们的目标是通过此方法测试 Lambda:
str -> {
Random rnd = new Random();
int nr = rnd.nextInt(str.length());
String ch = String.valueOf(str.charAt(nr));
return ch;
})
那么,让我们应用前面的三个步骤:
- 让我们用
static
方法提取这个 Lambda:
public static String extractCharacter(String str) {
Random rnd = new Random();
int nr = rnd.nextInt(str.length());
String chAsStr = String.valueOf(str.charAt(nr));
return chAsStr;
}
- 让我们用相应的方法引用替换 Lambda:
public List<String> rndStringFromStrings(List<String> strs) {
return strs.stream()
.map(StringOperations::extractCharacter)
.collect(Collectors.toList());
}
- 让我们测试一下
static
方法(即 Lambda):
@Test
public void testRndStringFromStrings() throws Exception {
String str1 = "Some";
String str2 = "random";
String str3 = "text";
String result1 = extractCharacter(str1);
String result2 = extractCharacter(str2);
String result3 = extractCharacter(str3);
assertEquals(result1.length(), 1);
assertEquals(result2.length(), 1);
assertEquals(result3.length(), 1);
assertThat(str1, containsString(result1));
assertThat(str2, containsString(result2));
assertThat(str3, containsString(result3));
}
建议避免使用具有多行代码的 Lambda。因此,通过遵循前面的技术,Lambda 变得易于测试。
179 调试 Lambda
在调试 Lambda 时,至少有三种解决方案:
- 检查栈跟踪
- 日志
- 依赖 IDE 支持(例如,NetBeans、Eclipse 和 IntelliJ IDEA 支持调试 Lambda,开箱即用或为其提供插件)
让我们把重点放在前两个方面,因为依赖 IDE 是一个非常大和具体的主题,不在本书的范围内。
检查 Lambda 或流管道中发生的故障的栈跟踪可能非常令人费解。让我们考虑以下代码片段:
List<String> names = Arrays.asList("anna", "bob", null, "mary");
names.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
因为这个列表中的第三个元素是null
,所以我们将得到一个NullPointerException
,并且定义流管道的整个调用序列都被公开,如下面的屏幕截图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MCvfX5Yc-1657285412192)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/1d5f65be-2910-42da-8f79-11ba3fa54a2f.png)]
突出显示的行告诉我们这个NullPointerException
发生在一个名为lambda$main$5
的 Lambda 表达式中。由于 Lambda 没有名称,因此此名称是由编译器编写的。此外,我们不知道哪个元素是null
。
因此,我们可以得出结论,报告 Lambda 或流管道内部故障的栈跟踪不是很直观。
或者,我们可以尝试记录输出。这将帮助我们调试流中的操作管道。这可以通过forEach()
方法实现:
List<String> list = List.of("anna", "bob",
"christian", "carmen", "rick", "carla");
list.stream()
.filter(s -> s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
这将为我们提供以下输出:
CARLA
CARMEN
CHRISTIAN
在某些情况下,这种技术可能很有用。当然,我们必须记住,forEach()
是一个终端操作,因此流将被消耗。因为一个流只能被消费一次,所以这可能是一个问题。
而且,如果我们在列表中添加一个null
值,那么输出将再次变得混乱。
一个更好的选择是依靠peek()
方法。这是一个中间操作,它对当前元素执行某个操作,并将该元素转发到管道中的下一个操作。下图显示了工作中的peek()
操作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PTin2eFM-1657285412193)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/3417a264-ab2a-4d7e-a0c7-e1c999a487a7.png)]
让我们看看代码形式:
System.out.println("After:");
names.stream()
.peek(p -> System.out.println("\tstream(): " + p))
.filter(s -> s.startsWith("c"))
.peek(p -> System.out.println("\tfilter(): " + p))
.map(String::toUpperCase)
.peek(p -> System.out.println("\tmap(): " + p))
.sorted()
.peek(p -> System.out.println("\tsorted(): " + p))
.collect(Collectors.toList());
以下是我们可能收到的输出示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ltppNt5J-1657285412194)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/ac5b81df-0a34-4fc7-90db-62e813798c01.png)]
现在,我们故意在列表中添加一个null
值,然后再次运行:
List<String> names = Arrays.asList("anna", "bob",
"christian", null, "carmen", "rick", "carla");
在向列表中添加一个null
值后获得以下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-suqfwO5Q-1657285412194)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/2a4b5c75-e4c9-41f5-9cd7-fcb8b592cbcf.png)]
这一次,我们可以看到在应用了stream()
之后出现了null
值。因为stream()
是第一个操作,所以我们可以很容易地发现错误存在于列表内容中。
180 过滤流中的非零元素
在第 8 章、“函数式编程——基础与设计模式”中,在“编写函数式接口”部分,我们定义了一个基于函数式接口Predicate
的filter()
方法。Java 流 API 已经有了这样的方法,函数式接口称为java.util.function.Predicate
。
假设我们有以下List
个整数:
List<Integer> ints = Arrays.asList(1, 2, -4, 0, 2, 0, -1, 14, 0, -1);
流式传输此列表并仅提取非零元素可以按如下方式完成:
List<Integer> result = ints.stream()
.filter(i -> i != 0)
.collect(Collectors.toList());
结果列表将包含以下元素:1、2、-4、2、-1、14、-1
下图显示了filter()
如何在内部工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpYRRoIw-1657285412195)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/50aeef03-4be8-43bd-a3db-7b158a66880e.png)]
注意,对于几个常见的操作,Java 流 API 已经提供了现成的中间操作。因此,不需要提供Predicate
。其中一些操作如下:
distinct()
:从流中删除重复项skip(
n)
:丢弃前n
个元素limit(
s)
:截断流长度不超过s
sorted()
:根据自然顺序对河流进行排序sorted(Comparator<? super T> comparator)
:根据给定的Comparator
对流进行排序
让我们将这些操作和一个filter()
添加到一个示例中。我们将过滤零,过滤重复项,跳过 1 个值,将剩余的流截断为两个元素,并按其自然顺序排序:
List<Integer> result = ints.stream()
.filter(i -> i != 0)
.distinct()
.skip(1)
.limit(2)
.sorted()
.collect(Collectors.toList());
结果列表将包含以下两个元素:-4
和2
。
下图显示了此流管道如何在内部工作:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNzPHJty-1657285412196)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/0c7d98fc-4e96-404d-b908-df834912a2b3.png)]
当filter()
操作需要复杂/复合或长期条件时,建议采用辅助static
方法提取,并依赖方法引用。因此,避免这样的事情:
List<Integer> result = ints.stream()
.filter(value -> value > 0 && value < 10 && value % 2 == 0)
.collect(Collectors.toList());
您应该更喜欢这样的内容(Numbers
是包含辅助方法的类):
List<Integer> result = ints.stream()
.filter(Numbers::evenBetween0And10)
.collect(Collectors.toList());
private static boolean evenBetween0And10(int value) {
return value > 0 && value < 10 && value % 2 == 0;
}
181 无限流、takeWhile()
和dropWhile()
在这个问题的第一部分,我们将讨论无限流。在第二部分中,我们将讨论takeWhile()
和dropWhile()
api。
无限流是无限期地创建数据的流。因为流是懒惰的,它们可以是无限的。更准确地说,创建无限流是作为中间操作完成的,因此在执行管道的终端操作之前,不会创建任何数据。
例如,下面的代码理论上将永远运行。此行为由forEach()
终端操作触发,并由缺少约束或限制引起:
Stream.iterate(1, i -> i + 1)
.forEach(System.out::println);
Java 流 API 允许我们以多种方式创建和操作无限流,您很快就会看到。
此外,根据定义的相遇顺序,可以有序或无序。流是否有相遇顺序取决于数据源和中间操作。例如,Stream
以List
作为其源,因为List
具有内在顺序,所以对其进行排序。另一方面,Stream
以Set
作为其来源是无序的,因为Set
不保证有序。一些中间操作(例如,sorted()
)可以向无序的Stream
施加命令,而一些终端操作(例如,forEach()
)可以忽略遭遇命令。
通常,顺序流的性能不受排序的显著影响,但是取决于所应用的操作,并行流的性能可能会受到顺序Stream
的存在的显著影响。
不要把Collection.stream().forEach()
和Collection.forEach()
混为一谈。虽然Collection.forEach()
可以依靠集合的迭代器(如果有的话)来保持顺序,Collection.stream().forEach()
的顺序没有定义。例如,通过list.forEach()
多次迭代List
将按插入顺序处理元素,而list.parallelStream().forEach()
在每次运行时产生不同的结果。根据经验,如果不需要流,则通过Collection.forEach()
对集合进行迭代。
我们可以通过BaseStream.unordered()
将有序流转化为无序流,如下例所示:
List<Integer> list
= Arrays.asList(1, 4, 20, 15, 2, 17, 5, 22, 31, 16);
Stream<Integer> unorderedStream = list.stream()
.unordered();
无限有序流
通过Stream.iterate(T seed, UnaryOperator<T> f)
可以得到无限的有序流。结果流从指定的种子开始,并通过将f
函数应用于前一个元素(例如,n
元素是f(n-1)
来继续)。
例如,类型 1、2、3、…、n 的整数流可以如下创建:
Stream<Integer> infStream =