Module java.base

Package java.util.stream


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

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

在这里,我们使用widgets,一个Collection<Widget>,作为流的源,然后对流执行过滤-映射-归约操作,以获得红色小部件的重量总和。 (求和是一个归约操作的示例。)

这个包引入的关键抽象是。类StreamIntStreamLongStreamDoubleStream是对象和原始intlongdouble类型的流。流在几个方面与集合不同:

  • 没有存储。流不是存储元素的数据结构;相反,它通过计算操作的管道从数据结构、数组、生成器函数或I/O通道等源传递元素。
  • 功能性质。对流的操作会产生一个结果,但不会修改其源。例如,对从集合获取的Stream进行过滤会产生一个新的没有被过滤元素的Stream,而不是从源集合中删除元素。
  • 追求延迟。许多流操作,如过滤、映射或去重,可以被实现为延迟执行,从而提供优化的机会。例如,“找到第一个具有三个连续元音字母的String”不需要检查所有输入字符串。流操作分为中间(生成Stream)操作和终端(生成值或副作用)操作。中间操作总是延迟执行。
  • 可能是无界的。虽然集合具有有限的大小,但流不需要。诸如limit(n)findFirst()之类的短路操作可以使对无限流的计算在有限时间内完成。
  • 可消费的。流的元素在流的生命周期内只被访问一次。与Iterator一样,必须生成一个新流才能重新访问源的相同元素。
可以通过多种方式获取流。一些示例包括:
  • 通过stream()parallelStream()方法从Collection获取;
  • 通过Arrays.stream(Object[])从数组获取;
  • 通过流类上的静态工厂方法,如Stream.of(Object[])IntStream.range(int, int)Stream.iterate(Object, UnaryOperator)
  • 可以从BufferedReader.lines()获取文件的行;
  • 可以从Files中的方法获取文件路径的流;
  • 可以从Random.ints()获取随机数的流;
  • JDK中还有许多其他带有流的方法,包括BitSet.stream()Pattern.splitAsStream(java.lang.CharSequence)JarFile.stream()

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

流操作和流水线

流操作分为中间终端操作,并组合成流水线。流水线由一个源(如Collection、数组、生成器函数或I/O通道);零个或多个中间操作,如Stream.filterStream.map;以及一个终端操作,如Stream.forEachStream.reduce组成。

中间操作返回一个新的流。它们总是延迟的;执行中间操作,如filter(),实际上不执行任何过滤,而是创建一个新的流,当遍历时,它包含与给定谓词匹配的初始流的元素。流水线源的遍历直到执行流水线的终端操作才开始。

终端操作,如Stream.forEachIntStream.sum,可能遍历流以生成结果或副作用。执行终端操作后,流水线被认为已被消耗,不能再使用;如果需要再次遍历相同的数据源,则必须返回数据源以获取新的流。在几乎所有情况下,终端操作是急切的,它们在返回之前会完成对数据源的遍历和流水线的处理。只有终端操作iterator()spliterator()不是急切的;它们被提供作为一种“逃生口”,以便在现有操作不足以完成任务时启用任意客户控制的流水线遍历。

懒处理流允许实现显著的效率;在上面的过滤-映射-求和示例中,过滤、映射和求和可以融合为数据的单次传递,中间状态最小。懒惰还允许在不必要时避免检查所有数据;对于“查找第一个长度超过1000个字符的字符串”等操作,只需要检查足够多的字符串以找到具有所需特征的一个,而不必检查源中可用的所有字符串。(当输入流是无限的而不仅仅是大时,这种行为变得更加重要。)

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

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

此外,一些操作被视为短路操作。如果中间操作在面对无限输入时可能产生有限流,则称其为短路操作。如果终端操作在面对无限输入时可能在有限时间内终止,则称其为短路操作。在流水线中具有短路操作是使处理无限流在有限时间内正常终止的必要条件,但不是充分条件。

并行性

使用显式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表达式。为了保持正确的行为,这些行为参数必须是非干扰的,在大多数情况下必须是无状态的。这些参数始终是函数接口的实例,如Function,通常是lambda表达式或方法引用。

非干扰性

流使您能够在各种数据源上执行可能并行的聚合操作,甚至包括非线程安全的集合,如ArrayList。只有在执行流水线期间可以防止干扰数据源时,才能实现这一点。除了逃生操作iterator()spliterator(),执行从终端操作被调用开始,直到终端操作完成结束。对于大多数数据源,防止干扰意味着确保在流水线执行期间完全不修改数据源。这一点的显著例外是那些数据源为并发集合的流,这些流专门设计用于处理并发修改。并发流源是那些其Spliterator报告CONCURRENT特征的流。

因此,在流水线中的行为参数可能不是并发的情况下,行为参数不应该修改流的数据源。如果行为参数修改或导致修改流的数据源,则称行为参数干扰非并发数据源。非干扰性的需求适用于所有流水线,而不仅仅是并行的。除非流源是并发的,在执行流水线期间修改流的数据源可能会导致异常、不正确的答案或不符合行为。对于行为良好的流源,可以在终端操作开始之前修改源,并且这些修改将反映在涵盖的元素中。例如,请考虑以下代码:


     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操作开始之前修改了列表,结果将是一个字符串“one two three”。JDK集合返回的所有流,以及大多数其他JDK类,都以这种方式表现良好;对于由其他库生成的流,请参阅低级流构造以获取构建行为良好流的要求。

无状态行为

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

     Set<Integer> seen = Collections.synchronizedSet(new HashSet<>());
     stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })...
 
在这里,如果映射操作并行执行,对于相同的输入,由于线程调度的差异,结果可能会因运行而异,而使用无状态lambda表达式,结果将始终相同。

还要注意,尝试从行为参数中访问可变状态会让您在安全性和性能方面面临一个糟糕的选择;如果不同步访问该状态,您将面临数据竞争,因此您的代码将出现问题,但如果同步访问该状态,您将面临争用,从而破坏您试图从中受益的并行性。最佳方法是完全避免对流操作使用有状态的行为参数;通常有一种方法可以重新构造流水线以避免有状态性。

副作用

在流操作的行为参数中使用副作用通常是不鼓励的,因为这往往会导致无意中违反无状态性要求,以及其他线程安全隐患。

如果行为参数具有副作用,除非明确声明,否则不能保证:

  • 这些副作用对其他线程的
  • 在相同流水线中对“相同”元素执行的不同操作是否在相同线程中执行;以及
  • 行为参数是否总是被调用,因为流实现可以省略流水线中的操作(或整个阶段),如果可以证明这不会影响计算结果。

副作用的顺序可能会令人惊讶。即使流水线被限制为生成与流源的遇到顺序一致的结果(例如,IntStream.range(0,5).parallel().map(x -> x*2).toArray()必须生成[0, 2, 4, 6, 8]),也不保证对单个元素应用映射函数的顺序,或者对于给定元素执行行为参数的线程。

省略副作用也可能会令人惊讶。除了终端操作forEachforEachOrdered之外,如果流实现可以优化掉行为参数的执行而不影响计算结果,则行为参数的副作用可能不会始终执行。(有关具体示例,请参阅count操作上记录的API注释。)

许多计算中,人们可能会倾向于使用副作用的地方,可以更安全、更高效地使用归约,而不是使用副作用,例如使用可变累加器。但是,例如用于调试目的的println()之类的副作用通常是无害的。少数流操作,如forEach()peek(),只能通过副作用操作;这些应谨慎使用。

以下代码示例展示了如何将一个不当使用副作用的流水线转换为不使用副作用的流水线,该代码搜索字符串流以匹配给定的正则表达式,并将匹配项放入列表中。


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

     List<String> results =
         stream.filter(s -> pattern.matcher(s).matches())
               .toList();  // 无副作用!
 

顺序

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

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

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

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

归约操作

一个归约操作(也称为折叠)接受一系列输入元素,并通过重复应用组合操作(例如找到一组数字的和或最大值,或将元素累积到列表中)将它们组合成单个摘要结果。流类具有多种形式的一般归约操作,称为reduce()collect(),以及多种专门的归约形式,如sum()max()count()

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


    int sum = 0;
    for (int x : numbers) {
       sum += x;
    }
 
但是,有很多理由更喜欢使用归约操作而不是像上面那样的可变累加。归约“更抽象”——它作用于整个流而不是单个元素——而且一个正确构造的归约操作本质上是可并行化的,只要用于处理元素的函数是可结合的无状态的。例如,给定一个数字流,我们想要找到总和,我们可以编写:

    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);
 

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

之前展示的“widgets”示例展示了减少如何与其他操作结合以用批量操作替换for循环。如果 widgets 是一个包含 Widget 对象的集合,这些对象具有一个 getWeight 方法,我们可以找到最重的小部件:


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

在更一般的形式中,对类型为 <T> 的元素进行 reduce 操作,产生类型为 <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)) 必须等于 accumulator.apply(u, t)

三参数形式是两参数形式的一般化,将映射步骤合并到累积步骤中。我们可以将简单的权重总和示例重新构建为更一般的形式:


     int sumOfWeights = widgets.stream()
                               .reduce(0,
                                       (sum, b) -> sum + b.getWeight(),
                                       Integer::sum);
 
虽然显式的映射-减少形式更易读,因此通常应优先使用。提供了广义形式,用于优化将映射和减少合并为单个函数的情况。

可变减少

一个 可变减少操作 将输入元素累积到可变结果容器中,例如 CollectionStringBuilder,在处理流中的元素时进行累积。

如果我们想将字符串流连接成一个长字符串,我们可以使用普通的减少操作来实现:


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

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

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


 <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());
     }
 
或者我们可以使用可并行化的 collect 形式:

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

     List<String> strings = stream.map(Object::toString)
                                  .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
 
这里,我们的供应商只是 ArrayList 构造函数,累加器将字符串化的元素添加到一个 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);
 
(第二个类型参数的 ? 仅表示我们不关心此收集器使用的中间表示。)如果我们想创建一个按部门统计工资总和的收集器,我们可以重用 summingSalaries 使用 groupingBy

     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 中)对于某些 Map 实现可能是昂贵的。

但是,假设此减少中使用的结果容器是一个可以并发修改的集合 -- 例如 ConcurrentHashMap。在这种情况下,累加器的并行调用实际上可以同时将其结果存入相同的共享结果容器中,从而消除了组合器合并不同结果容器的需要。这可能会提高并行执行性能。我们将其称为 并发 减少。

支持并发减少的Collector被标记为具有Collector.Characteristics.CONCURRENT特征。然而,并发收集也有一个缺点。如果多个线程同时将结果存入共享容器,则结果存入的顺序是不确定的。因此,只有在流程处理时不重要的情况下,才能进行并发减少。如果流程是并行的,收集器具有Collector.Characteristics.CONCURRENT特征,并且流程是无序的,或者收集器具有Collector.Characteristics.UNORDERED特征,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的并发等效版本)。

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

结合性

如果以下条件成立,则运算符或函数op结合的

     (a op b) op c == a op (b op c)
 
如果我们将这个扩展到四个术语,可以看到这对并行评估的重要性:

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

结合性操作的示例包括数字加法、最小值、最大值和字符串连接。

低级流构造

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

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

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

更高质量的spliterator将提供平衡和已知大小的拆分、准确的大小信息以及spliterator或数据的其他一些特征,这些特征可以被实现用来优化执行。

对于可变数据源的Spliterators存在额外的挑战;绑定到数据的时间,因为数据可能在创建spliterator和执行流程管道之间发生变化。理想情况下,流的spliterator将报告IMMUTABLECONCURRENT的特征;如果不能,它应该是延迟绑定的。如果源不能直接提供推荐的spliterator,它可以间接地使用Supplier提供spliterator,并通过Supplier接受版本的stream()构建流。只有在流程管道的终端操作开始后,才从供应商那里获取spliterator。

这些要求显著减少了流源的变异和流程管道执行之间的干扰的潜在范围。基于具有所需特征的spliterators的流,或者使用基于Supplier的工厂形式的流,对于终端操作开始前对数据源的修改是免疫的(前提是流操作的行为参数符合非干扰和无状态性所需标准)。有关更多详细信息,请参阅非干扰

自Java版本:
1.8
  • Class
    Description
    BaseStream<T,S extends BaseStream<T,S>>
    流的基本接口,这些流是支持顺序和并行聚合操作的元素序列。
    Collector<T,A,R>
    一个可变的归约操作,将输入元素累积到可变结果容器中,可选择在处理所有输入元素后将累积的结果转换为最终表示。
    表示Collector的属性,可用于优化归约实现。
    实现了各种有用的归约操作的Collector的实现,例如将元素累积到集合中,根据各种标准对元素进行总结等。
    一系列支持顺序和并行聚合操作的原始双精度元素序列。
    用于DoubleStream的可变构建器。
    表示接受double值参数和DoubleConsumer的操作,并且不返回结果。
    一系列支持顺序和并行聚合操作的原始整数值元素序列。
    用于 的可变构建器。
    表示接受int值参数和IntConsumer的操作,并且不返回结果。
    一系列支持顺序和并行聚合操作的原始长整型元素序列。
    用于 的可变构建器。
    表示接受long值参数和LongConsumer的操作,并且不返回结果。
    Stream<T>
    一系列支持顺序和并行聚合操作的元素序列。
    用于Stream的可变构建器。
    用于创建和操作流的低级实用方法。