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

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

本章包括 22 个涉及 Java 函数式编程的问题。这里,我们将重点讨论在流中遇到的涉及经典操作的几个问题(例如,filtermap),并讨论无限流、空安全流和缺省方法。这个问题的综合列表将涵盖分组、分区和收集器,包括 JDK12teeing()收集器和编写自定义收集器。此外,还将讨论takeWhile()dropWhile()、组合函数、谓词和比较器、Lambda 测试和调试以及其他一些很酷的话题。

一旦您涵盖了本章和上一章,您就可以在生产应用上释放函数式编程了。下面的问题将为您准备各种各样的用例,包括角落用例或陷阱。

问题

使用以下问题来测试您的函数式编程能力。我强烈建议您在使用解决方案和下载示例程序之前,先尝试一下每个问题:

  1. 测试高阶函数:编写几个单元测试来测试所谓的高阶函数。
  2. 使用 Lambda 的测试方法:为使用 Lambda 的测试方法编写几个单元测试。
  3. 调试 Lambda:提供一种调试 Lambda 的技术。
  4. 过滤流中的非零元素:编写流管道,过滤流中的非零元素。
  5. 无限流的takeWhile()dropWhile():编写几个处理无限流的代码片段。另外,写几个使用takeWhile()dropWhile()API 的例子。
  6. 流的映射:写几个通过map()flatMap()映射流的例子。
  7. 查找流中的不同元素:编写查找流中不同元素的程序。
  8. 匹配流中不同元素:编写一个匹配流中不同元素的程序。
  9. 流的总和、最大、最小:通过StreamStream.reduce()的原始类型特化编写计算给定流的总和、最大、最小的程序。
  10. 收集流的结果:编写一些代码片段,用于收集列表、映射和集合中的流的结果。
  11. 连接流的结果:写几个代码片段,将流的结果连接到String中。
  12. 摘要收集器:写几个代码片段来展示摘要收集器的用法。
  13. 分组:编写用于处理groupingBy()收集器的代码片段。
  14. 分区:编写几个代码片段,用于使用partitioningBy()收集器。
  15. 过滤、展开和映射收集器:编写几段代码,举例说明过滤、展开和映射收集器的用法。
  16. Teeing:编写几个合并两个收集器(JDK12 和Collectors.teeing()的结果的示例。
  17. 编写自定义收集器:编写一个表示自定义收集器的程序。
  18. 方法引用:写一个方法引用的例子。
  19. 流的并行处理:简要介绍流的并行处理。分别为parallelStream()parallel()spliterator()提供至少一个示例。
  20. 空安全流:编写一个程序,从元素或元素集合返回空安全流。
  21. 组合函数、谓词和比较器:编写几个组合函数、谓词和比较器的示例。
  22. 默认方法:编写一个包含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 本身。这个问题的解决方案包括三个主要步骤:

  1. static方法提取 Lambda
  2. 方法引用替换 Lambda
  3. 测试这个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;
})

那么,让我们应用前面的三个步骤:

  1. 让我们用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;
}
  1. 让我们用相应的方法引用替换 Lambda:
public List<String> rndStringFromStrings(List<String> strs) {
  return strs.stream()
    .map(StringOperations::extractCharacter)
    .collect(Collectors.toList());
}
  1. 让我们测试一下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 章、“函数式编程——基础与设计模式”中,在“编写函数式接口”部分,我们定义了一个基于函数式接口Predicatefilter()方法。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 comparator):根据给定的Comparator对流进行排序

让我们将这些操作和一个filter()添加到一个示例中。我们将过滤零,过滤重复项,跳过 1 个值,将剩余的流截断为两个元素,并按其自然顺序排序:

List<Integer> result = ints.stream()
  .filter(i -> i != 0)
  .distinct()
  .skip(1)
  .limit(2)
  .sorted()
  .collect(Collectors.toList());

结果列表将包含以下两个元素:-42

下图显示了此流管道如何在内部工作:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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

相关文章
|
10月前
|
IDE Java 编译器
java编程最基础学习
Java入门需掌握:环境搭建、基础语法、面向对象、数组集合与异常处理。通过实践编写简单程序,逐步深入学习,打牢编程基础。
473 1
|
10月前
|
Java
如何在Java中进行多线程编程
Java多线程编程常用方式包括:继承Thread类、实现Runnable接口、Callable接口(可返回结果)及使用线程池。推荐线程池以提升性能,避免频繁创建线程。结合同步与通信机制,可有效管理并发任务。
341 6
|
10月前
|
安全 前端开发 Java
从反射到方法句柄:深入探索Java动态编程的终极解决方案
从反射到方法句柄,Java 动态编程不断演进。方法句柄以强类型、低开销、易优化的特性,解决反射性能差、类型弱、安全性低等问题,结合 `invokedynamic` 成为支撑 Lambda 与动态语言的终极方案。
371 0
|
11月前
|
Java 开发者
Java 函数式编程全解析:静态方法引用、实例方法引用、特定类型方法引用与构造器引用实战教程
本文介绍Java 8函数式编程中的四种方法引用:静态、实例、特定类型及构造器引用,通过简洁示例演示其用法,帮助开发者提升代码可读性与简洁性。
|
11月前
|
SQL Java 数据库
2025 年 Java 从零基础小白到编程高手的详细学习路线攻略
2025年Java学习路线涵盖基础语法、面向对象、数据库、JavaWeb、Spring全家桶、分布式、云原生与高并发技术,结合实战项目与源码分析,助力零基础学员系统掌握Java开发技能,从入门到精通,全面提升竞争力,顺利进阶编程高手。
1457 2
|
11月前
|
Java 大数据 API
Java Stream API:现代集合处理与函数式编程
Java Stream API:现代集合处理与函数式编程
448 100
|
11月前
|
Java 开发者
Java并发编程:CountDownLatch实战解析
Java并发编程:CountDownLatch实战解析
645 100
|
11月前
|
NoSQL Java 关系型数据库
超全 Java 学习路线,帮你系统掌握编程的超详细 Java 学习路线
本文为超全Java学习路线,涵盖基础语法、面向对象编程、数据结构与算法、多线程、JVM原理、主流框架(如Spring Boot)、数据库(MySQL、Redis)及项目实战等内容,助力从零基础到企业级开发高手的进阶之路。
706 1
|
存储 并行计算 Java
Java中的函数式编程与Lambda表达式探索
Java中的函数式编程与Lambda表达式探索
|
并行计算 Java API
Java中的函数式编程实战与Lambda表达式应用
Java中的函数式编程实战与Lambda表达式应用