模块 java.base

包 java.util.stream


java.util.stream
支持对元素流进行函数式操作的类,例如对集合的 map-reduce 转换。例如:

   int sum = widgets.stream()
           .filter(b -> b.getColor() == RED)
           .mapToInt(b -> b.getWeight())
           .sum();
  

这里我们使用 widgets ,一个 Collection<Widget> 作为流的来源,然后对流执行 filter-map-reduce 以获得红色小部件的权重之和。 (求和是 减少 操作的示例。)

此包中引入的关键抽象是 stream 。类 Stream IntStream LongStream DoubleStream 是基于对象和原始 intlongdouble 类型的流。流在几个方面不同于集合:

  • 没有存储空间。流不是存储元素的数据结构;相反,它通过计算操作的管道从源(例如数据结构、数组、生成器函数或 I/O 通道)传送元素。
  • 本质上是功能性的。流上的操作会产生结果,但不会修改其源。例如,过滤从集合中获得的 Stream 会生成一个没有过滤元素的新 Stream,而不是从源集合中删除元素。
  • 懒惰寻求。许多流操作,如过滤、映射或重复删除,可以延迟实现,从而提供优化机会。例如,“找到第一个具有三个连续元音的String”不需要检查所有输入字符串。流操作分为中间(Stream 生产)操作和终端(值或副作用生产)操作。中间操作总是懒惰的。
  • 可能是无界的。虽然集合的大小是有限的,但流不需要。诸如 limit(n)findFirst() 之类的短路操作可以让对无限流的计算在有限时间内完成。
  • 消耗品。流的元素在流的生命周期中仅被访问一次。与 Iterator 一样,必须生成新流以重新访问源的相同元素。
可以通过多种方式获得流。一些示例包括:

第三方库可以使用 这些技术 提供额外的流源。

流操作和管道

Stream操作分为intermediateterminal操作,组合起来形成stream pipelines。流管道由源(例如 Collection 、数组、生成器函数或 I/O 通道)组成;随后是零个或多个中间操作,例如 Stream.filterStream.map ;和终端操作,例如 Stream.forEachStream.reduce

中间操作返回一个新流。他们总是lazy;执行诸如 filter() 之类的中间操作实际上并不执行任何过滤,而是创建一个新流,该流在遍历时包含与给定谓词匹配的初始流的元素。管道源的遍历直到管道的终端操作被执行后才开始。

终端操作(例如 Stream.forEachIntStream.sum )可能会遍历流以产生结果或副作用。执行终端操作后,stream pipeline被认为消耗掉了,不能再使用;如果需要再次遍历同一个数据源,则必须返回数据源获取新的流。在几乎所有情况下,终端操作都是 eager ,在返回之前完成对数据源的遍历和管道的处理。只有终端操作iterator()spliterator()不是;这些是作为“逃生舱口”提供的,以便在现有操作不足以完成任务的情况下启用任意客户端控制的管道遍历。

延迟处理流可以显着提高效率;在上面的 filter-map-sum 示例等管道中,过滤、映射和求和可以融合到数据的单次传递中,中间状态最少。懒惰还可以避免在不必要时检查所有数据;对于诸如“查找第一个超过 1000 个字符的字符串”之类的操作,只需检查足够的字符串以找到具有所需特征的字符串,而无需检查源中可用的所有字符串。 (当输入流是无限的而不仅仅是大的时候,这种行为变得更加重要。)

中间操作进一步分为statelessstateful操作。无状态操作,例如 filtermap,在处理新元素时不会保留先前看到的元素的状态——每个元素都可以独立于其他元素上的操作进行处理。有状态操作,例如 distinctsorted ,在处理新元素时可能会合并以前看到的元素的状态。

有状态操作可能需要在产生结果之前处理整个输入。例如,在看到流的所有元素之前,无法对流进行排序产生任何结果。因此,在并行计算下,一些包含有状态中间操作的管道可能需要对数据进行多次传递,或者可能需要缓冲重要数据。只包含无状态中间操作的流水线可以单次处理,无论是顺序的还是并行的,数据缓冲最少。

此外,某些操作被视为 short-circuiting 操作。如果一个中间操作在有无限输入时可能会产生一个有限的流,那么它就是短路的。如果终端操作在无限输入时可能在有限时间内终止,则该终端操作是短路的。在管道中进行短路操作是无限流的处理在有限时间内正常终止的必要条件,但不是充分条件。

平行度

具有显式 for- 循环的处理元素本质上是串行的。流通过将计算重新定义为聚合操作的管道而不是对每个单独元素的命令式操作来促进并行执行。所有流操作都可以串行或并行执行。 JDK 中的流实现创建串行流,除非明确要求并行性。例如,Collection 有方法 Collection.stream() Collection.parallelStream() ,它们分别产生顺序流和并行流;其他承载流的方法(例如 IntStream.range(int, int) )会生成顺序流,但可以通过调用它们的 BaseStream.parallel() 方法来有效地并行化这些流。要并行执行先前的“小部件权重总和”查询,我们会这样做:


   int sumOfWeights = widgets.parallelStream()
                .filter(b -> b.getColor() == RED)
                .mapToInt(b -> b.getWeight())
                .sum();
  

此示例的串行和并行版本之间的唯一区别是初始流的创建,使用“parallelStream()”而不是“stream()”。流管道是顺序执行还是并行执行,具体取决于调用终端操作的流的模式。可以使用BaseStream.isParallel() 方法确定流的顺序或并行模式,并且可以使用BaseStream.sequential() BaseStream.parallel() 操作修改流的模式。最近的顺序或并行模式设置适用于整个流管道的执行。

除了标识为明确不确定的操作(例如 findAny() ),流是顺序执行还是并行执行不应更改计算结果。

大多数流操作接受描述用户指定行为的参数,这些参数通常是 lambda 表达式。为了保持正确的行为,这些 behavioral parameters 必须是 non-interfering ,并且在大多数情况下必须是 stateless 。此类参数始终是 功能接口 的实例,例如 Function ,并且通常是 lambda 表达式或方法引用。

不干涉

Streams 使您能够对各种数据源执行可能并行的聚合操作,甚至包括非线程安全的集合,例如 ArrayList 。这只有在我们可以在流管道执行期间防止interference与数据源发生冲突时才有可能。除了 escape-hatch 操作 iterator()spliterator() ,执行在调用终端操作时开始,在终端操作完成时结束。对于大多数数据源来说,防止干扰就是保证流水线执行过程中的数据源是not modified at all。一个明显的例外是源是并发集合的流,它们专门设计用于处理并发修改。并发流源是那些 Spliterator 报告 CONCURRENT 特性的源。

因此,源可能不是并发的流管道中的行为参数永远不应该修改流的数据源。如果行为参数修改或导致修改流的数据源,则称其为具有非并发数据源的interfere。不干扰的需要适用于所有管道,而不仅仅是并行管道。除非流源是并发的,否则在流管道执行期间修改流的数据源可能会导致异常、不正确的答案或不一致的行为。对于行为良好的流源,可以在终端操作开始之前修改源,这些修改将反映在所覆盖的元素中。例如,考虑以下代码:


   List<String> l = new ArrayList(Arrays.asList("one", "two"));
   Stream<String> sl = l.stream();
   l.add("three");
   String s = sl.collect(joining(" "));
  
首先创建一个包含两个字符串的列表:“one”和“two”。然后从该列表创建一个流。接下来通过添加第三个字符串修改列表:“three”。最后,流的元素被收集并连接在一起。由于列表在终端 collect 操作开始之前被修改,结果将是一个字符串“一二三”。从 JDK 集合和大多数其他 JDK 类返回的所有流都以这种方式表现良好;对于其他库生成的流,请参阅 低级流构建 以了解构建行为良好的流的要求。

无状态行为

如果流操作的行为参数为 stateful ,则流管道结果可能不确定或不正确。有状态 lambda(或其他实现适当功能接口的对象)的结果取决于流管道执行期间可能发生变化的任何状态。有状态 lambda 的一个示例是 map() 中的参数:

   Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
   stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })...
  
在这里,如果map操作并行执行,由于线程调度差异,同一输入的结果可能会因运行而异,而无状态 lambda expression 结果总是一样的。

另请注意,尝试从行为参数访问可变状态会给您带来安全和性能方面的错误选择;如果您不同步对该状态的访问,则会出现数据竞争,因此您的代码会被破坏,但如果您同步对该状态的访问,则存在竞争破坏您寻求从中受益的并行性的风险。最好的方法是避免有状态的行为参数完全流式操作;通常有一种方法可以重组流管道以避免有状态。

副作用

通常,不鼓励对流操作的行为参数产生副作用,因为它们通常会导致无意中违反无状态要求,以及其他线程安全隐患。

如果行为参数确实有副作用,除非明确说明,否则无法保证:

  • 这些副作用对其他线程的 能见度
  • 同一流管道中“相同”元素的不同操作在同一线程中执行;和
  • 行为参数总是被调用,因为流实现可以自由地从流管道中删除操作(或整个阶段),如果它可以证明它不会影响计算结果的话。

副作用的顺序可能令人惊讶。即使管道被限制生成与流源的遇到顺序一致的 result(例如,IntStream.range(0,5).parallel().map(x -> x*2).toArray() 必须生成 [0, 2, 4, 6, 8]),也无法保证将映射器函数应用于各个元素的顺序,或者在哪个线程中为给定元素执行任何行为参数。

副作用的消除也可能令人惊讶。除了终端操作 forEach forEachOrdered ,当流实现可以在不影响计算结果的情况下优化行为参数的执行时,行为参数的副作用可能并不总是被执行。 (有关具体示例,请参阅记录在 count 操作中的 API 说明。)

许多可能会使用副作用的计算可以更安全有效地表达而没有副作用,例如使用 减少 而不是可变累加器。但是,使用 println() 进行调试等副作用通常是无害的。少量流操作,例如 forEach()peek() ,只能通过副作用进行操作;这些应该小心使用。

作为如何将不适当使用副作用的流管道转换为不使用副作用的示例,以下代码在字符串流中搜索与给定正则表达式匹配的字符串,并将匹配项放入列表中。


   ArrayList<String> results = new ArrayList<>();
   stream.filter(s -> pattern.matcher(s).matches())
      .forEach(s -> results.add(s)); // Unnecessary use of side-effects!
  
此代码不必要地使用了副作用。如果并行执行,ArrayList 的非线程安全性将导致不正确的结果,并且添加所需的同步将导致争用,从而破坏并行性的优势。此外,在这里使用副作用是完全没有必要的; forEach() 可以简单地替换为更安全、更高效且更适合并行化的归约操作:

   List<String> results =
     stream.filter(s -> pattern.matcher(s).matches())
        .toList(); // No side-effects!
  

订购

流可能有也可能没有定义的 encounter order 。流是否有相遇顺序取决于源和中间操作。某些流源(例如 List 或数组)本质上是有序的,而其他流源(例如 HashSet )则不是。一些中间操作,例如 sorted() ,可能会在其他无序流上施加遇到顺序,而其他操作可能会使有序流呈现无序,例如 BaseStream.unordered() 。此外,某些终端操作可能会忽略遇到顺序,例如 forEach()

如果一个流是有序的,那么大多数操作都被限制在它们遇到的顺序中对元素进行操作;如果流的源是包含 [1, 2, 3]List,则执行 map(x -> x*2) 的结果必须是 [2, 4, 6]。但是,如果源没有定义的遭遇顺序,则值 [2, 4, 6] 的任何排列都将是有效结果。

对于顺序流,遇到顺序的存在与否不会影响性能,只会影响确定性。如果一个流是有序的,在相同的源上重复执行相同的流管道将产生相同的结果;如果没有排序,重复执行可能会产生不同的结果。

对于并行流,放宽顺序约束有时可以实现更高效的执行。如果元素的排序不相关,则可以更有效地实现某些聚合操作,例如过滤重复项 (distinct()) 或分组缩减 (Collectors.groupingBy())。同样,本质上与遇到顺序相关的操作(例如 limit() )可能需要缓冲以确保正确排序,从而破坏并行性的好处。在流有遇到顺序的情况下,但用户并不特别care关于那个遇到顺序,使用unordered() 显式地取消顺序流可能会提高某些有状态或终端操作的并行性能。然而,大多数流管道,例如上面的“块权重之和”示例,即使在顺序约束下仍然可以有效地并行化。

还原操作

reduction 操作(也称为 fold )采用一系列输入元素,并通过重复应用组合操作(例如查找一组数字的总和或最大值,或将元素累加到列表中)将它们组合成单个汇总结果.流类具有多种形式的通用缩减操作,称为 reduce() collect() ,以及多种专门的缩减形式,例如 sum() max() count()

当然,这样的操作可以很容易地实现为简单的顺序循环,如下所示:


  int sum = 0;
  for (int x : numbers) {
    sum += x;
  }
  
但是,有充分的理由更喜欢 reduce 操作而不是上述的可变累积。不仅归约“更抽象”——它作为一个整体而不是单个元素对流进行操作——而且正确构造的归约操作本质上是可并行化的,只要用于处理元素的函数是联想的无国籍的。例如,给定一个我们想要求和的数字流,我们可以写:

  int sum = numbers.stream().reduce(0, (x,y) -> x+y);
  
或:

  int sum = numbers.stream().reduce(0, Integer::sum);
  

这些归约操作可以安全地并行运行,几乎无需修改:


  int sum = numbers.parallelStream().reduce(0, Integer::sum);
  

Reduction 并行化很好,因为实现可以并行地对数据的子集进行操作,然后组合中间结果以获得最终的正确答案。 (即使该语言具有“parallel for-each”构造,可变累积方法仍将要求开发人员为共享累积变量 sum 提供线程安全更新,然后所需的同步可能会消除并行性带来的任何性能提升.) 改为使用 reduce() 消除了并行化归约操作的所有负担,并且该库可以提供高效的并行实现,而无需额外的同步。

前面显示的“小部件”示例显示了缩减如何与其他操作相结合,以用批量操作替换 for 循环。如果 widgetsWidget 对象的集合,它有一个 getWeight 方法,我们可以找到最重的小部件:


   OptionalInt heaviest = widgets.parallelStream()
                  .mapToInt(Widget::getWeight)
                  .max();
  

在其更一般的形式中,reduce 对类型为 <T> 的元素的操作产生类型为 <U> 的结果需要三个参数:


 <U> U reduce(U identity,
       BiFunction<U, ? super T, U> accumulator,
       BinaryOperator<U> combiner);
  
在这里,identity 元素既是缩减的初始种子值,也是没有输入元素时的默认结果。 accumulator 函数采用部分结果和下一个元素,并生成新的部分结果。 combiner 函数组合两个部分结果以产生新的部分结果。 (组合器在并行缩减中是必需的,其中输入被分区,为每个分区计算部分累加,然后组合部分结果以产生最终结果。)

更正式地说,identity 值必须是组合器函数的 identity。这意味着对于所有 ucombiner.apply(identity, u) 等于 u 。此外,combiner 函数必须是 联想的,并且必须与 accumulator 函数兼容:对于所有 utcombiner.apply(u, accumulator.apply(identity, t)) 必须是 equals()accumulator.apply(u, t)

三参数形式是二参数形式的推广,将映射步骤合并到累加步骤中。我们可以使用更一般的形式重铸简单的权重和示例,如下所示:


   int sumOfWeights = widgets.stream()
                .reduce(0,
                    (sum, b) -> sum + b.getWeight(),
                    Integer::sum);
  
尽管显式的 map-reduce 形式更具可读性,因此通常应该是首选。为可以通过将映射和归约组合成单个函数来优化大量工作的情况提供了通用形式。

可变减少

mutable reduction operation 将输入元素累积到可变结果容器中,例如 CollectionStringBuilder ,因为它处理流中的元素。

如果我们想获取字符串流并将它们连接成一个长字符串,我们可以could通过普通归约来实现:


   String concatenated = strings.reduce("", String::concat)
  

我们会得到想要的结果,它甚至可以并行工作。但是,我们可能对性能不满意!这样的实现会进行大量的字符串复制,并且运行时的字符数为 O(n^2)。一种更高效的方法是将结果累积到 StringBuilder 中,这是一个用于累积字符串的可变容器。我们可以使用与普通归约相同的技术来并行化可变归约。

可变缩减操作称为 collect() ,因为它将所需结果收集到一个结果容器中,例如 Collectioncollect 操作需要三个函数:构造结果容器新实例的供应函数,将输入元素合并到结果容器中的累加器函数,以及将一个结果容器的内容合并到另一个结果容器中的组合函数。这种形式与普通归约的一般形式非常相似:


 <R> R collect(Supplier<R> supplier,
        BiConsumer<R, ? super T> accumulator,
        BiConsumer<R, R> combiner);
  

reduce() 一样,以这种抽象方式表达 collect 的一个好处是它可以直接进行并行化:我们可以并行累加部分结果,然后将它们组合起来,只要累加和组合函数满足适当的要求即可。例如,要将流中元素的字符串表示收集到 ArrayList 中,我们可以编写明显的顺序 for-each 形式:


   ArrayList<String> strings = new ArrayList<>();
   for (T element : stream) {
     strings.add(element.toString());
   }
  
或者我们可以使用可并行化的收集形式:

   ArrayList<String> strings = stream.collect(() -> new ArrayList<>(),
                        (c, e) -> c.add(e.toString()),
                        (c1, c2) -> c1.addAll(c2));
  
或者,将map操作从累加器函数中提取出来,我们可以将其更简洁地表达为:

   List<String> strings = stream.map(Object::toString)
                 .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
  
这里,我们的供应商只是 ArrayList constructor ,累加器将字符串化元素添加到 ArrayList ,组合器只是使用 addAll 将字符串从一个容器复制到另一个容器。

collect 的三个方面——供应商、累加器和组合器——紧密耦合。我们可以使用 Collector 的抽象来捕获所有三个方面。上面将字符串收集到 List 的示例可以使用标准 Collector 重写为:


   List<String> strings = stream.map(Object::toString)
                 .collect(Collectors.toList());
  

将可变归约打包到 Collector 中还有另一个优势:可组合性。类 Collectors 包含许多预定义的收集器工厂,包括将一个收集器转换为另一个收集器的组合器。例如,假设我们有一个收集器来计算员工流的工资总和,如下所示:


   Collector<Employee, ?, Integer> summingSalaries
     = Collectors.summingInt(Employee::getSalary);
  
(第二个类型参数的 ? 仅表示我们不关心此收集器使用的中间表示。)如果我们想创建一个收集器来按部门列出工资总和,我们可以使用 groupingBy 重用 summingSalaries

   Map<Department, Integer> salariesByDept
     = employees.stream().collect(Collectors.groupingBy(Employee::getDepartment,
                              summingSalaries));
  

与常规归约操作一样,collect() 操作只有在满足适当条件的情况下才能并行化。对于任何部分累积的结果,将其与空结果容器组合必须产生等效结果。也就是说,对于作为任何系列累加器和组合器调用的结果的部分累加结果 pp 必须等同于 combiner.apply(p, supplier.get())

此外,无论计算如何拆分,它都必须产生等效的结果。对于任何输入元素 t1t2,下面计算中的结果 r1r2 必须相等:


   A a1 = supplier.get();
   accumulator.accept(a1, t1);
   accumulator.accept(a1, t2);
   R r1 = finisher.apply(a1); // result without splitting

   A a2 = supplier.get();
   accumulator.accept(a2, t1);
   A a3 = supplier.get();
   accumulator.accept(a3, t2);
   R r2 = finisher.apply(combiner.apply(a2, a3)); // result with splitting
  

这里,等价一般是指根据 Object.equals(Object) 。但在某些情况下,可能会放宽等效性以解决顺序差异。

缩减、并发和排序

对于一些复杂的归约操作,例如生成 Mapcollect(),例如:

   Map<Buyer, List<Transaction>> salesByBuyer
     = txns.parallelStream()
        .collect(Collectors.groupingBy(Transaction::getBuyer));
  
并行执行操作实际上可能适得其反。这是因为合并步骤(通过密钥将一个 Map 合并到另一个)对于某些 Map 实现来说可能很昂贵。

然而,假设在这个缩减中使用的结果容器是一个可并发修改的集合——比如 ConcurrentHashMap 。在那种情况下,累加器的并行调用实际上可以将它们的结果并发存放到同一个共享结果容器中,从而消除了组合器合并不同结果容器的需要。这可能会提高并行执行性能。我们称之为concurrent减少。

支持并发缩减的 Collector 标有 Collector.Characteristics.CONCURRENT 特性。然而,并发收集也有一个缺点。如果多个线程同时将结果存放到共享容器中,则存放结果的顺序是不确定的。因此,只有在排序对于正在处理的流不重要的情况下,才有可能进行并发缩减。 Stream.collect(Collector) 实现只会在以下情况下执行并发缩减

您可以使用 BaseStream.unordered() 方法确保流是无序的。例如:

   Map<Buyer, List<Transaction>> salesByBuyer
     = txns.parallelStream()
        .unordered()
        .collect(groupingByConcurrent(Transaction::getBuyer));
  
(其中 Collectors.groupingByConcurrent(java.util.function.Function<? super T, ? extends K>) groupingBy 的并发等价物)。

请注意,如果给定键的元素按照它们在源中出现的顺序出现很重要,那么我们不能使用并发归约,因为排序是并发插入的牺牲品之一。然后,我们将被迫实现顺序缩减或基于合并的并行缩减。

结合性

如果以下条件成立,则运算符或函数 opassociative

   (a op b) op c == a op (b op c)
  
如果我们将其扩展为四个项,则可以看出它对并行评估的重要性:

   a op b op c op d == (a op b) op (c op d)
  
因此我们可以与 (c op d) 并行评估 (a op b),然后在结果上调用 op

关联运算的示例包括数字加法、最小值和最大值以及字符串连接。

低级流构建

到目前为止,所有的流示例都使用了类似Collection.stream() Arrays.stream(Object[]) 的方法来获取流。那些流的方法是如何实现的?

StreamSupport 有许多用于创建流的低级方法,所有方法都使用某种形式的 Spliterator 。拆分器是 Iterator 的并行模拟;它描述了一个(可能是无限的)元素集合,支持顺序推进、批量遍历以及将输入的一部分拆分到另一个可以并行处理的拆分器中。在最低级别,所有流都由拆分器驱动。

在实现拆分器时有许多实现选择,几乎所有选择都是在实现简单性和使用该拆分器的流的运行时性能之间进行权衡。创建拆分器的最简单但性能最低的方法是使用 Spliterators.spliteratorUnknownSize(java.util.Iterator, int) 从迭代器创建拆分器。虽然这样的拆分器可以工作,但它可能会提供较差的并行性能,因为我们已经丢失了大小信息(基础数据集有多大),并且受限于简单的拆分算法。

更高质量的拆分器将提供平衡和已知大小的拆分、准确的大小信息,以及拆分器的许多其他 characteristics 或数据,可用于实现优化执行。

可变数据源的拆分器有一个额外的挑战;绑定到数据的时间,因为数据可能会在创建拆分器的时间和执行流管道的时间之间发生变化。理想情况下,流的拆分器会报告 IMMUTABLECONCURRENT 的特征;如果不是,它应该是 late-binding 。如果源不能直接提供推荐的拆分器,它可以使用 Supplier 间接提供拆分器,并通过 Supplier 接受版本的 stream() 构造流。分离器仅在流管道的终端操作开始后才从供应商处获得。

这些要求显着减少了流源突变和流管道执行之间的潜在干扰范围。基于具有所需特征的拆分器的流,或使用基于供应商的工厂表单的流,在终端操作开始之前不受数据源修改的影响(前提是流操作的行为参数满足非-干扰和无国籍)。有关详细信息,请参阅 不干涉

自从:
1.8
  • 描述
    BaseStream <T,S 扩展 BaseStream <T,S>>
    流的基本接口,它是支持顺序和并行聚合操作的元素序列。
    Collector <T,A,R>
    一个 可变归约操作 将输入元素累积到可变结果容器中,在处理完所有输入元素后,可选择将累积结果转换为最终表示。
    指示 Collector 属性的特征,可用于优化缩减实现。
    Collector 的实现实现了各种有用的归约操作,例如将元素累积到集合中、根据各种条件汇总元素等。
    支持顺序和并行聚合操作的原始双值元素序列。
    DoubleStream 的可变构建器。
    表示接受 double 值参数和 DoubleConsumer 且不返回任何结果的操作。
    支持顺序和并行聚合操作的原始 int 值元素序列。
    IntStream 的可变构建器。
    表示接受 int 值参数和 IntConsumer 且不返回任何结果的操作。
    支持顺序和并行聚合操作的原始长值元素序列。
    LongStream 的可变构建器。
    表示接受 long 值参数和 LongConsumer 且不返回任何结果的操作。
    Stream <T>
    支持顺序和并行聚合操作的元素序列。
    Stream 的可变构建器。
    用于创建和操作流的低级实用方法。