资讯详情

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

原文:Java Coding Problems

协议:CC BY-NC-SA 4.0

贡献者:飞龙

本文来自【ApacheCN Java 谷歌翻译用谷歌翻译。

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

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

问题

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

  1. :编写几个单元测试来测试所谓的高级函数。
  2. :为使用 Lambda 编写几个单元测试的测试方法。
  3. :提供调试 Lambda 的技术。
  4. :编写流管,过滤流中的非零元素。
  5. :编写几个处理无限流的代码片段。另外,写几个使用takeWhile()dropWhile()API 的例子。
  6. :写几个通过map()flatMap()映射流的例子。
  7. :在搜索流中编写不同元素的程序。
  8. :编写匹配流中不同元素的程序。
  9. :通过StreamStream.reduce()编写和计算给定流的总和、最大和最小程序的原始类型。
  10. :编写一些代码片段,用于收集列表、映射和集合中的流的结果。
  11. :写几个代码片段,连接流结果String中。
  12. :写几个代码片段来显示摘要收集器的用法。
  13. :编写用于处理groupingBy()收集器的代码片段。
  14. :编写几个代码片段,用于使用partitioningBy()收集器。
  15. :编写几段代码,例如过滤、扩展和映射收集器的使用。
  16. :编写几个合并两个收集器(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<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 本身。这个问题的解决方案包括三个主要步骤:

  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<? super T> 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;
}

181 无限流、takeWhile()dropWhile()

在这个问题的第一部分,我们将讨论无限流。在第二部分中,我们将讨论takeWhile()dropWhile()api。

无限流是无限期地创建数据的流。因为流是懒惰的,它们可以是无限的。更准确地说,创建无限流是作为中间操作完成的,因此在执行管道的终端操作之前,不会创建任何数据。

例如,下面的代码理论上将永远运行。此行为由forEach()终端操作触发,并由缺少约束或限制引起:

Stream.iterate(1, i -> i + 1)
  .forEach(System.out::println);

Java 流 API 允许我们以多种方式创建和操作无限流,您很快就会看到。

此外,根据定义的相遇顺序,可以有序无序。流是否有相遇顺序取决于数据源和中间操作。例如,StreamList作为其源,因为List具有内在顺序,所以对其进行排序。另一方面,StreamSet作为其来源是无序的,因为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 = 

标签: 连接器q18j4adf37nc连接器ba附带连接器0123连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台