本章包括 22 个涉及 Java 函数式编程的问题。这里,我们将重点讨论在流中遇到的涉及经典操作的几个问题(例如,filter和map),并讨论无限流、空安全流和缺省方法。这个问题的综合列表将涵盖分组、分区和收集器,包括 JDK12teeing()收集器和编写自定义收集器。此外,还将讨论takeWhile()、dropWhile()、组合函数、谓词和比较器、Lambda 测试和调试以及其他一些很酷的话题。
一旦您涵盖了本章和上一章,您就可以在生产应用上释放函数式编程了。下面的问题将为您准备各种各样的用例,包括角落用例或陷阱。
问题
使用以下问题来测试您的函数式编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:
- 测试高阶函数:编写几个单元测试来测试所谓的高阶函数。
- 使用 Lambda 的测试方法:为使用 Lambda 的测试方法编写几个单元测试。
- 调试 Lambda:提供一种调试 Lambda 的技术。
- 过滤流中的非零元素:编写流管道,过滤流中的非零元素。
- 无限流的
takeWhile()和dropWhile():编写几个处理无限流的代码片段。另外,写几个使用takeWhile()和dropWhile()API 的例子。 - 流的映射:写几个通过
map()和flatMap()映射流的例子。 - 查找流中的不同元素:编写查找流中不同元素的程序。
- 匹配流中不同元素:编写一个匹配流中不同元素的程序。
- 流的总和、最大、最小:通过
Stream和Stream.reduce()的原始类型特化编写计算给定流的总和、最大、最小的程序。 - 收集流的结果:编写一些代码片段,用于收集列表、映射和集合中的流的结果。
- 连接流的结果:写几个代码片段,将流的结果连接到
String中。 - 摘要收集器:写几个代码片段来展示摘要收集器的用法。
- 分组:编写用于处理
groupingBy()收集器的代码片段。 - 分区:编写几个代码片段,用于使用
partitioningBy()收集器。 - 过滤、展开和映射收集器:编写几段代码,举例说明过滤、展开和映射收集器的用法。
- Teeing:编写几个合并两个收集器(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的行为,如下所示:
@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):截断流长度不超过ssorted():根据自然顺序对河流进行排序sorted(Comparator 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; }
Java 编程问题:九、函数式编程——深入研究2https://developer.aliyun.com/article/1426155