10.4 新增的Stream方法

问题

用户希望使用 Java 9 为 Stream 接口添加的新特性。

方案

使用 Stream 接口新增的 ofNullableiteratetakeWhile 以及 dropWhile 方法。

讨论

Java 9 为 Stream 接口引入了 ofNullableiteratetakeWhiledropWhile 等新方法,本范例将讨论它们的用法。

  • ofNullable方法

在 Java 8 中,of 方法包括两种形式,一种传入单个值,另一种传入可变参数列表。无论哪种形式,参数都不能为空。

而在 Java 9 中,新的 ofNullable 方法可以在参数不为空时返回一个包装值的单元素流,为空时返回一个空流。例 10-16 的用例显示了该方法的应用。

例 10-16 ofNullable 方法的应用

  1. @Test
  2. public void ofNullable() throws Exception {
  3. Stream<String> stream = Stream.ofNullable("abc");
  4. assertEquals(1, stream.count());
  5.  
  6. stream = Stream.ofNullable(null);
  7. assertEquals(0, stream.count());
  8. }

❶ 单元素流

❷ 返回 Stream.empty()

在本例中,count 方法返回流中非空元素的数量,我们可以借此在任何参数上使用 ofNullable 方法,而无须首先检查参数是否为空。

  • 传入Predicateiterate方法

另一种有趣的方法是 iterate。在 Java 8 中,iterate 方法的签名如下:

  1. static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)

创建流时,从初始元素种子开始,对种子依次应用一元运算符以产生后续元素。由于生成的流是一个无限流,通常需要采用 limit 或其他短路操作(short-circuiting operation,如 findFirstfindAny)来控制返回流的大小。

在 Java 9 中,iterate 方法新增了一种重载形式,它传入 Predicate 作为第二个参数:

  1. static<T> Stream<T> iterate(T seed, Predicate<? super T> hasNext,
  2. UnaryOperator<T> next)

创建流时,从初始元素种子开始,对种子应用一元运算符,直至值不再满足谓词 hasNext

相关应用如例 10-17 所示。

例 10-17 传入 Predicateiterate 方法

  1. @Test
  2. public void iterate() throws Exception {
  3. List<BigDecimal> bigDecimals =
  4. Stream.iterate(BigDecimal.ZERO, bd -> bd.add(BigDecimal.ONE))
  5. .limit(10)
  6. .collect(Collectors.toList());
  7.  
  8. assertEquals(10, bigDecimals.size());
  9.  
  10. bigDecimals = Stream.iterate(BigDecimal.ZERO,
  11. bd -> bd.longValue() < 10L,
  12. bd -> bd.add(BigDecimal.ONE))
  13. .collect(Collectors.toList());
  14.  
  15. assertEquals(10, bigDecimals.size());
  16. }

❶ 创建 BigDecimal 流(Java 8 实现)

❷ 创建 BigDecimal 流(Java 9 实现)

第一个流使用 iterate 方法,并通过 limit 方法控制大小,这是 Java 8 的实现方式;第二个流传入 Predicate 作为第二个参数,看起来更像是传统的 for 循环。

  • takeWhiledropWhile方法

Java 9 新增了 takeWhiledropWhile 方法,二者基于谓词获取流的某一部分。根据 Javadoc 的描述,对于有序流,takeWhile 方法从流的起始位置开始,返回“匹配给定谓词的元素的最长前缀”。

dropWhile 方法的作用正好相反,在丢弃匹配给定谓词的元素的最长前缀后,该方法将返回流的其余元素。

两种方法在有序流上的应用如例 10-18 所示。

例 10-18 获取与丢弃流中的元素

  1. @Test
  2. public void takeWhile() throws Exception {
  3. List<String> strings = Stream.of("this is a list of strings".split(" "))
  4. .takeWhile(s -> !s.equals("of"))
  5. .collect(Collectors.toList());
  6. List<String> correct = Arrays.asList("this", "is", "a", "list");
  7. assertEquals(correct, strings);
  8. }
  9.  
  10. @Test
  11. public void dropWhile() throws Exception {
  12. List<String> strings = Stream.of("this is a list of strings".split(" "))
  13. .dropWhile(s -> !s.equals("of"))
  14. .collect(Collectors.toList());
  15. List<String> correct = Arrays.asList("of", "strings");
  16. assertEquals(correct, strings);
  17. }

❶ 当不再满足谓词时,返回谓词之前的元素

❷ 当不再满足谓词时,返回谓词之后的元素

可以看到,两种方法在同一个位置将流拆分,不过 takeWhile 返回拆分位置之前的元素,而 dropWhile 返回拆分位置之后的元素。

takeWhile 方法的最大优点在于它是一种短路操作:对于一个包含大量排序元素的集合,只要达到所设定的条件,就可以停止求值。

例如,假设存在一个由客户订单构成的集合,集合中的元素以降序方式排序。借由 takeWhile 方法,我们可以只获取高于某个阈值的订单,而不必筛选每个元素。

例 10-19 模拟了这种情况。程序生成 50 个 0 到 100 之间的随机整数,对它们做降序排序,然后仅返回大于 90 的整数。

例 10-19 对整数流应用 takeWhile 方法

  1. Random random = new Random();
  2. List<Integer> nums = random.ints(50, 0, 100)
  3. .boxed()
  4. .sorted(Comparator.reverseOrder())
  5. .takeWhile(n -> n > 70)
  6. .collect(Collectors.toList());

❶ 生成 50 个 0 到 100 之间的随机整数

❷ 将这些整数装箱,以便采用 Comparator 排序并收集

❸ 将流拆分并返回大于 90 的整数

改用 dropWhile 方法或许能让本例看起来更为直观(尽管效率未必会提高),如例 10-20 所示。

例 10-20 对整数流应用 dropWhile 方法

  1. Random random = new Random();
  2. List<Integer> nums = random.ints(50, 0, 100)
  3. .sorted()
  4. .dropWhile(n -> n < 90)
  5. .boxed()
  6. .collect(Collectors.toList());

❶ 升序排序

❷ 将流拆分并返回大于 90 的整数

类似 takeWhiledropWhile 这样的方法在其他语言中已存在多年,Java 9 将二者正式引入 Java。