查找流的特征
流的特征
流 API 依赖于一个特殊的对象,它是 Spliterator
接口的实例。该接口的名称来源于这样一个事实,即在流 API 中,分隔器扮演的角色类似于在集合 API 中迭代器的角色。此外,由于流 API 支持并行处理,分隔器对象还控制着流如何将元素拆分到处理并行化的不同 CPU 之间。该名称是 split 和 iterator 的缩写。
详细介绍这个分隔器对象超出了本教程的范围。您需要知道的是,这个分隔器对象包含流的 特征。这些特征不是您经常会用到的东西,但了解它们是什么将有助于您在某些情况下编写更好、更高效的管道。
流的特征如下。
特征 | 注释 |
---|---|
ORDERED | 流中元素的处理顺序很重要。 |
DISTINCT | 该流处理的元素中没有重复项。 |
NONNULL | 该流中没有空元素。 |
SORTED | 该流的元素已排序。 |
SIZED | 该流处理的元素数量已知。 |
SUBSIZED | 拆分该流会产生两个 SIZED 流。 |
本教程中没有介绍 IMMUTABLE 和 CONCURRENT 这两个特征。
每个流在创建时都会设置或取消设置所有这些特征。
请记住,流可以通过两种方式创建。
- 您可以从数据源创建流,我们已经介绍了几种不同的模式。
- 每次您在现有流上调用中间操作时,都会创建一个新的流。
给定流的特征取决于它创建的源,或者创建它的流的特征,以及创建它的操作。如果您的流是使用源创建的,那么它的特征将取决于该源,如果您使用另一个流创建它,那么它们将取决于这个另一个流以及您使用的操作类型。
让我们更详细地介绍每个特征。
有序流
ORDERED 流是使用有序数据源创建的。第一个想到的例子可能是 List
接口的任何实例。还有其他例子:Files.lines(path)
和 Pattern.splitAsStream(string)
也会生成 ORDERED 流。
跟踪流中元素的顺序可能会导致并行流的开销。如果您不需要此特征,那么可以通过在现有流上调用 unordered()
中间方法来删除它。这将返回一个没有此特征的新流。为什么要这样做?在某些情况下,保持流 ORDERED 可能代价高昂,例如当您使用并行流时。
排序流
一个 SORTED 流是一个已排序的流。该流可以从排序的源创建,例如 TreeSet
实例,或者通过调用 sorted()
方法。知道流已经排序可以被流实现用来避免再次排序已经排序的流。这种优化可能不会一直使用,因为 SORTED 流可能会再次使用与第一次不同的比较器进行排序。
有一些中间操作会清除 SORTED 特征。在以下代码中,您可以看到 strings
和 filteredStream
都是 SORTED 流,而 lengths
不是。
Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");
Stream<String> strings = stringCollection.stream().sorted();
Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
Stream<Integer> lengths = filteredStrings.map(String::length);
映射或扁平映射 SORTED 流会从结果流中删除此特征。
不同的流
一个 DISTINCT 流是一个流,它处理的元素中没有重复项。当从 HashSet
实例创建流时,或者从调用 distinct()
中间方法调用时,会获得此特征。
当过滤流时,DISTINCT 特征会保留,但在映射或扁平映射流时会丢失。
让我们检查以下示例。
Collection<String> stringCollection = List.of("one", "two", "two", "three", "four", "five");
Stream<String> strings = stringCollection.stream().distinct();
Stream<String> filteredStrings = strings.filtered(s -> s.length() < 5);
Stream<Integer> lengths = filteredStrings.map(String::length);
stringCollection.stream()
不是 DISTINCT,因为它是由List
的实例构建的。strings
是 DISTINCT,因为该流是通过调用distinct()
中间方法创建的。filteredStrings
仍然是 DISTINCT:从流中删除元素不会创建重复项。length
已被映射,因此 DISTINCT 特征丢失了。
非空流
一个 NONNULL 流是一个流,它不包含空值。集合框架中有一些结构不接受空值,包括 ArrayDeque
和并发结构,如 ArrayBlockingQueue
、ConcurrentSkipListSet
以及通过调用 ConcurrentHashMap.newKeySet()
返回的并发集。使用 Files.lines(path)
和 Pattern.splitAsStream(line)
创建的流也是 NONNULL 流。
与之前的特征一样,一些中间操作可以生成具有不同特征的流。
- 过滤或排序一个 NONNULL 流将返回一个 NONNULL 流。
- 在 NONNULL 流上调用
distinct()
也将返回一个 NONNULL 流。 - 映射或扁平映射一个 NONNULL 流将返回一个没有此特性的流。
大小和子大小流
大小流
当您想要使用并行流时,最后一个特性非常重要。并行流将在本教程的后面部分详细介绍。
一个 SIZED 流是一个知道将要处理多少元素的流。从任何 Collection
实例创建的流都是这样的流,因为 Collection
接口有一个 size()
方法,因此获取这个数字很容易。
另一方面,在某些情况下,您知道您的流将处理有限数量的元素,但除非您处理流本身,否则您无法知道这个数字。
对于使用 Files.lines(path)
模式创建的流,情况就是这样。您可以获取文本文件的大小(以字节为单位),但此信息不会告诉您该文本文件有多少行。您需要分析文件才能获取此信息。
对于 Pattern.splitAsStream(line)
模式,情况也是如此。知道您正在分析的字符串中有多少个字符并不能提供任何关于该模式将产生多少个元素的提示。
子大小流
SUBSIZED 特性与流作为并行流计算时被拆分的方式有关。简而言之,并行化机制将流拆分为两部分,并在 CPU 执行的不同的可用内核之间分配计算。此拆分由流使用的 Spliterator
实例实现。此实现取决于您正在使用的源数据。
假设您需要在一个 ArrayList
上打开一个流。此列表的所有数据都保存在您的 ArrayList
实例的内部数组中。也许您还记得 ArrayList
对象的内部数组是一个紧凑的数组,因为当您从该数组中删除一个元素时,所有后续元素都会向左移动一个单元格,这样就不会留下任何空洞。
这使得拆分一个 ArrayList
很简单。要拆分一个 ArrayList
实例,您只需将该内部数组拆分为两部分,两部分中的元素数量相同。这使得在 ArrayList
实例上创建的流成为 SUBSIZED:您可以在拆分后提前知道每个部分将包含多少个元素。
现在假设您需要在一个 HashSet
实例上打开一个流。一个 HashSet
将其元素存储在一个数组中,但该数组的使用方式与 ArrayList
使用的数组不同。实际上,可以在该数组的给定单元格中存储多个元素。拆分该数组没有问题,但您无法在不计数的情况下提前知道每个部分将包含多少个元素。即使您将该数组从中间拆分,您也永远无法确定两半中是否会有相同数量的元素。这就是为什么在 HashSet
实例上创建的流是 SIZED 但不是 SUBSIZED 的原因。
转换流可能会改变返回流的 SIZED 和 SUBSIZED 特性。
- 映射和排序流将保留 SIZED 和 SUBSIZED 特性。
- 扁平映射、过滤和调用
distinct()
将删除这些特性。
上次更新: 2021 年 9 月 14 日