JDK8的集合流式操作

流操作分为中间终端操作,并且组合以形成流管线 。 流管线由源(例如Collection ,阵列,发生器功能或I / O通道)组成; 其次是零个或多个中间操作,如Stream.filterStream.map ; 以及诸如Stream.forEachStream.reduce终端Stream.reduce

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

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

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

中间操作进一步分为 状态有状态操作。 无状态操作(例如filtermap )在处理新元素时不保留先前看到的元素的状态 - 每个元素可以独立于其他元素的操作进行处理。 诸如distinctsorted有状态操作可以在处理新元件时结合先前看到的元素的状态。

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

此外,一些操作被认为是短路操作。 如果使用无限输入,则可以产生有限流,因此中间操作是短路的。 如果当无限输入呈现时,终端操作可能会在有限时间内终止。 在流水线中进行短路操作是处理无限流在有限时间内正常终止的必要但不足够的条件。

并行性

具有显式for-循环的处理元素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()。 当终端操作被启动时,根据被调用的流的方向,顺序或并行地执行流管道。 流是否以串行或并行方式执行可以使用isParallel()方法确定,并且可以使用BaseStream.sequential()BaseStream.parallel()操作修改流的方向。 当终端操作被启动时,根据其被调用的流的模式,顺序地或并行地执行流流水线。

除了确定为非确定性的操作,如findAny()流是顺序还是并行执行,不应该改变计算结果。

大多数流操作接受描述用户指定行为的参数,这些行为通常是lambda表达式。 为了保持正确的行为,这些行为参数必须是非干扰的 ,在大多数情况下必须是无状态的 。 这样的参数总是functional interface的例子 ,如Function ,并且通常是lambda表达式或方法引用

非干扰的

Streams使您能够对各种数据源执行可能并行的聚合操作,包括甚至非线程安全的集合,如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(" "));  

首先创建一个由两个字符串组成的列表:“一个”; 和“二”。 然后从该列表创建流。 接下来,通过添加第三个字符串“three”修改列表。 最后,流的元素被收集并连接在一起。 由于列表在终端collect操作开始之前被修改,所以结果将是一个“一二三”的字符串。 从JDK集合返回的所有流和大多数其他JDK类都以这种方式表现良好; 对于其他库生成的流,请参阅Low-level stream construction ,了解构建良好流的要求。

有状态的行为

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

   Set<Integer> seen = Collections.synchronizedSet(new HashSet<>()); stream.parallel().map(e -> { if (seen.add(e)) return 0; else return e; })...  

这里,如果并行执行映射操作,由于线程调度的差异,相同输入的结果可能因运行而异,而使用无状态的lambda表达式,结果总是相同的

还要注意,尝试从行为参数访问可变状态会给您带来安全和性能方面的不良选择; 如果您不同步对该状态的访问,则会有数据竞争,因此您的代码已损坏,但是如果您同步访问该状态,则冒有争议的风险有可能破坏您正在寻求的并行性。 最好的方法是避免有状态的行为参数完全流式传输; 通常有一种重组流管道以避免状态的方法。

副作用

一般而言,流行为的行为参数的副作用是不鼓励的,因为它们经常会导致无意识地违反无状态要求以及其他线程安全危害

如果行为参数确实有副作用,除非明确说明,有没有保证,而在visibility的那些副作用给其他线程,也没有任何保证相同的流管道内的“相同”的元素在不同的操作在同一个线程中执行。 此外,这些效果的排序可能是令人惊讶的。 即使当管道被限制以产生与流源的遇到顺序一致的结果 (例如, IntStream.range(0,5).parallel().map(x -> x*2).toArray()必须产生[0, 2, 4, 6, 8] )时,不保证将映射器功能应用于各个元件的顺序,或者在什么线程中为给定元素执行任何行为参数。

许多可能诱惑使用副作用的计算可以更安全有效地表达而无副作用,例如使用reduction代替可变累加器。 但是,对于调试目的使用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()可以简单地替换为更安全,更高效,更适合并行化的还原操作:collection()

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

顺序

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

如果一个流被命令,大多数操作被限制为在遇到的顺序中对元素进行操作; 如果流的源是List含有[1, 2, 3] ,然后执行的结果map(x -> x*2)必须是[2, 4, 6] 。 然而,如果源没有定义的遇到顺序,则值[2, 4, 6]任何[2, 4, 6]将是有效结果。

对于顺序流,遇到顺序的存在或不存在不影响性能,仅影响确定性。 如果流被排序,在相同的源上重复执行相同的流管线将产生相同的结果; 如果没有订购,重复执行可能会产生不同的结果。

对于并行流,放宽排序约束有时可以实现更有效的执行。 某些聚合操作,例如过滤重复( distinct() )或组合减少( Collectors.groupingBy() )可以更有效地实现,如果元素的排序不相关。 类似地,本质上与遇到顺序相关的操作,如limit()可能需要缓冲以确保正确排序,从而破坏并行性的好处。 另外,当流有遭遇订单,但用户并不特别在意那次偶遇秩序的情况下,明确地去订货与流tunorder()可以提高某些状态或终端操作的并行性能。 然而,大多数流管线,例如上面的例子的“权重之和”,仍然在订购限制下有效地并行化。

缩减操作

(也称为折叠 )采用一系列输入元素,并通过重复应用组合操作将其组合成单个摘要结果,例如查找一组数字的和或最大值,或将元素累加到列表中。 该流的类具有普遍减少操作,所谓的多种形式reduce()collect() ,以及多个专业化还原的形式,如sum()max() ,或count()

Reduction, concurrency, and ordering

对于一些复杂的减少操作,例如一个collect() ,其产生Map ,如:

   Map<Buyer, List<Transaction>> salesByBuyer = txns.parallelStream() .collect(Collectors.groupingBy(Transaction::getBuyer));  

实际上并行执行操作可能会适得其反。 这是因为组合步骤(将一个Map合并成另一个按键)对于一些Map实现可能是昂贵的。

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

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

请注意,如果给定键的元素以它们在源中显示的顺序显示很重要,那么我们不能使用并发缩减,因为排序是并发插入的一种伤害。 然后,我们将被限制为执行顺序减少或基于并行的并行还原。

创建流的方式

流可以通过多种方式获得。 一些例子包括:

java.util.stream

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JDKJava Development Kit)是用于开发Java应用程序的软件开发工具包。在JDK 17和JDK 8之间进行选择时,需要考虑使用的Java版本以及项目的要求。 JDK 17是Java 17的开发工具包,它是最新的稳定版本。它提供了许多新的功能和改进,包括增强的性能、更好的内存管理和安全性,以及新的语言特性。此外,JDK 17还提供了对最新的Java SE规范(Java标准版规范)的支持,允许开发人员使用最新的Java技术。 相比之下,JDK 8是Java 8的开发工具包,是一个相对较旧但仍然广泛使用的版本。它包含了许多Java开发所需的基本工具和库,并提供了对Java SE 8规范的支持。特别是,JDK 8引入了Lambda表达式、函数式接口和流式编程等功能,这些功能使得编写和处理数据集合更加简洁和高效。 当选择下载适合的JDK版本时,应综合考虑以下因素: 1. 项目要求:如果项目对最新的Java技术有需求,或者需要利用JDK 17中引入的新特性,那么选择JDK 17可能是较好的选择。如果项目已经在JDK 8上开发,并且不需要新特性,可以选择继续使用JDK 8。 2. 兼容性:JDK 17在某些方面可能不兼容之前的JDK版本,因此,如果项目中使用了特定于JDK 8的功能或库,需要进行适应性调整和测试,以确保在JDK 17上可用。 3. 支持和更新:JDK 17是一个迭代的版本,将来可能会有更多的更新和改进。另一方面,JDK 8是一个相对稳定的版本,已经得到了广泛的支持和使用。如果对支持和更长期的维护有需求,JDK 8可能是一个更好的选择。 总而言之,选择下载JDK 17或JDK 8取决于项目需求和对Java技术的要求。如果需要利用最新的Java功能和改进,可以选择JDK 17。如果项目已经在JDK 8上开发,并且不需要新特性,可以继续使用JDK 8。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值