1. 概述

Java 8 的主要新特性之一是 Stream API。本教程将探讨一个有趣的话题:Stream.of()IntStream.range() 之间的差异。

2. 问题介绍

我们可以使用 Stream.of() 方法初始化一个 Stream 对象,例如 Stream.of(1, 2, 3, 4, 5)。如果想要初始化整数流,IntStream 类型更为直接,例如 IntStream.range(1, 6)。然而,通过这两种方式创建的整数流的行为可能会有所不同。

通常,我们会通过一个例子来理解这个问题。首先,让我们用两种不同的方式创建两个流:

Stream<Integer> normalStream = Stream.of(1, 2, 3, 4, 5);
IntStream intStreamByRange = IntStream.range(1, 6);

接下来,我们在这两个流上执行相同的操作:

STREAM.peek(add to a result list)
  .sorted()
  .findFirst();

在每个流上分别调用了三个方法:

  • peek() - 使用收集器收集处理过的元素到结果列表中
  • 排序元素
  • 从流中获取第一个元素

由于两个流包含相同的整数元素,我们预期执行后,两个结果列表也应包含相同的整数。因此,接下来我们将编写一个测试来检查是否得到预期的结果:

List<Integer> normalStreamPeekResult = new ArrayList<>();
List<Integer> intStreamPeekResult = new ArrayList<>();

// First, the regular Stream
normalStream.peek(normalStreamPeekResult::add)
  .sorted()
  .findFirst();
assertEquals(Arrays.asList(1, 2, 3, 4, 5), normalStreamPeekResult);

// Then, the IntStream
intStreamByRange.peek(intStreamPeekResult::add)
  .sorted()
  .findFirst();
assertEquals(Arrays.asList(1), intStreamPeekResult);

执行后,我们发现由 normalStream.peek() 填充的结果列表包含了所有整数元素,而由 intStreamByRange.peek() 填充的列表只包含一个元素。

现在,让我们来看看为什么会这样。

3. 流是惰性的

在解释为什么之前的测试中两个流产生了不同结果列表之前,先理解 Java 流程是设计上的懒惰是很重要的。

“懒惰”意味着流只有在被要求产生结果时才会执行所需的操作。换句话说,流的中间操作不会在执行终端操作时执行。这种懒惰行为可以是一种优势,因为它允许更高效的处理并防止不必要的计算。

为了快速理解这种懒惰行为,让我们暂时去掉之前测试中的 sort() 方法调用,然后重新运行:

List<Integer> normalStreamPeekResult = new ArrayList<>();
List<Integer> intStreamPeekResult = new ArrayList<>();

// First, the regular Stream
normalStream.peek(normalStreamPeekResult::add)
  .findFirst();
assertEquals(Arrays.asList(1), normalStreamPeekResult);

// Then, the IntStream
intStreamByRange.peek(intStreamPeekResult::add)
  .findFirst();
assertEquals(Arrays.asList(1), intStreamPeekResult);

这次,两个流都只填充了对应结果列表的第一个元素。这是因为 findFirst() 方法是终端操作,只需要第一个元素——也就是第一个排序后的元素。

现在我们了解了流是懒惰的,接下来我们将弄清楚当 sorted() 方法加入后,为什么两个结果列表会不同。

4. 调用 sorted() 可能使流变得“贪婪”

首先,看看由 Stream.of() 初始化的流。终端操作 findFirst() 只需要流中的第一个整数,但这里的“第一个”是在 sorted() 操作之后的。

我们知道,我们需要遍历所有整数来排序它们。因此,调用 sorted() 将流变成了“贪婪”。所以,**peek() 方法会在每个元素上被调用。**

另一方面,IntStream.range() 返回一个顺序排列的 IntStream。也就是说,IntStream 对象的输入已经是排序好的。此外,当对已排序的输入进行排序时,Java 会应用优化使其 sorted() 操作变为无操作(noop)。因此,结果列表中仍然只有一个元素。

接下来,让我们看一个基于 TreeSet 的流的例子:

List<String> peekResult = new ArrayList<>();

TreeSet<String> treeSet = new TreeSet<>(Arrays.asList("CCC", "BBB", "AAA", "DDD", "KKK"));

treeSet.stream()
  .peek(peekResult::add)
  .sorted()
  .findFirst();

assertEquals(Arrays.asList("AAA"), peekResult);

我们知道 TreeSet 是一个排序集合。因此,尽管我们调用了 sorted(),但我们看到 peekResult 列表中只有一个字符串。

5. 总结

在这篇文章中,我们以 Stream.of()IntStream.range() 为例,探讨了调用 sorted() 可能使流从“懒惰”变为“贪婪”的情况。

如往常一样,文章中展示的所有代码片段都可以在 GitHub 上找到。