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 上找到。