Java 8新特性-stream流 - Go语言中文社区

Java 8新特性-stream流


Stream流

Stream使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。Stream有以下特性及优点:

  • 无存储。Stream不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
  • 为函数式编程而生。对Stream的任何修改都不会修改背后的数据源,比如对Stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新Stream。
  • 惰式执行。Stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • 可消费性。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

对于流的处理,主要有三种关键性操作:

  1. 流的创建
  2. 中间操作(intermediate operation)
  3. 最终操作(terminal operation)

Stream类关系

从源码的角度来看,Stream接口继承于BaseStream接口。可以看到接口的签名:

public interface Stream<T> extends BaseStream<T, Stream<T>>

而如果结合BaseStream的签名:

public interface BaseStream<T, S extends BaseStream<T, S>> extends AutoCloseable

可以分析出这些泛型的作用:T是指定了流中元素数据类型,而S则表明了流的具体类型必然为BaseStream的子类。

而可以看到,BaseStream继承了AutoCloseable,这个类可以百度一下,可以自动关闭资源,所以在进行流操作的时候,不像IO操作那样需要手动调用close()方法。

而BaseStream有比较重要的一些子类,Stream是其中一个,还有IntStream、DoubleStream等。因为例如Stream<Integer>这样的写法还是很多的,而自动拆/装箱着实影响了效率,所以才额外准备了这些特殊的Stream类。

流的创建

通过已有的集合来创建流

在Java 8中,除了增加了很多Stream相关的类以外,还对集合类自身做了增强,在其中增加了stream方法,可以将一个集合类转换成流。例如:

Stream<Integer> stream = list.stream();

通过Stream创建流

可以使用Stream类提供的方法,直接返回一个由指定元素组成的流。例如of()方法:

Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7);

Stream中间操作

Stream有很多中间操作,多个中间操作可以连接起来形成一个流水线,每一个中间操作就像流水线上的一个工人,每人工人都可以对流进行加工,加工后得到的结果还是一个流。
常用中间操作列表:

终端操作会消费当前Stream流,并且会产生一个结果,有点类似管道了。而一个Stream流被消费过了,那它就不能被重用。也就是说,流操作是不可逆的。

每次中间操作会产生另一个流。但中间操作不是立即发生的。而是当在中间操作创建的新流上执行完最终操作后,中间操作指定的操作才会发生。这是一种惰性的。所以我们不能简单的理解为“流水线”模式。

流的中间操作还分无状态操作和有状态操作两种。例如过滤操作,因为每个元素都是被单独进行处理的,所有它和流中的其它元素无关。所以是无状态操作。

而例如排序,依赖于其他的元素,是有状态的操作。

filter

filter方法用于通过设置的条件过滤出元素。例如过滤掉空字符串:

list.stream().filter(string -> !string.isEmpty()).forEach(string -> System.out.println(string));

filter内的lambda表达式是返回的一个boolean数据类型,也就是说,若返回false,则该元素则需要被过滤掉。反之,则被加入到之后的流中。

map

用于映射每个元素到对应的结果。以下代码片段使用map输出了元素对应的平方数:

list.stream().map(i -> i*i).forEach(i -> System.out.println(i));

一般map都是作了一个映射。而Stream中的map则是将元素作为key,将对元素的处理作为value,构成映射关系。例如上面的map中的lambda表达式,以i作为key,认为是所有的元素,而对所有元素的处理结果则是求平方。

可以将map理解为:遍历当前流,作操作后再将其放在另一个流中。这样理解就不太有映射的感觉了。

limit/skip

limit返回stream的前n个元素,skip则是扔掉前n个元素。下面代码是在上面代码基础上,仅要1个元素:

list.stream().map(i -> i*i).limit(1).forEach(i -> System.out.println(i));

sorted

sorted方法用于对流进行排序:

list.stream().sorted().forEach(i -> System.out.println(i));

默认自然是升序排序。

distinct

distinct主要用来去重:

list.stream().distinct().forEach(i -> System.out.println(i));

stream最终操作

stream的中间操作得到的结果还是一个stream,那么如何把一个stream转换成我们需要的类型?比如计算出流中元素的个数、将流装换成集合等。这就需要最终操作(terminal operation)
最终操作会消耗流,产生一个最终结果。也就是说,在最终操作之后,不能再次使用流,也不能在使用任何中间操作,否则将抛出异常:

java.lang.IllegalStateException: stream has already been operated upon or closed

常用的最终操作如下图:

forEach

就是作为一种遍历,但是个人认为这里的forEach和Java 8新增的对集合的增强forEach是不一样的,一个是对流操作,一个是对集合操作。

count

用来统计个数,直接返回int。

collect

collect就是一个归约操作,可以接受Collectors的子类,将流中的元素累积成一个汇总结果,而Colectors是一个工具类,常用的方法是:

  • Collectors.toList()
  • Collectors.toConcurrentMap()
  • Collectors.toMap()
  • Collectors.toSet()

而在使用上:

List<Integer> newList = list.stream().sorted().collect(Collectors.toList());

collect会根据具体的接受子类,而返回一个具体的数据结构。

缩减操作

有点类似于count,其作为一种统计,最终只返回一个数量。但是缩减操作是指将元素缩减成一个特殊的结果,例如maxmin,即返回流中最大值元素于最小值元素。

同时,缩减操作也属于最终操作。得到的是一个具体的数据,其数据类型认为和流内元素的数据类型一致。

但是这两个是特殊的缩减操作,通用缩减操作最主要的还是reduce。

reduce

reduce可以理解为积累的过程。例如:实现对集合中元素遍历求和。正常的写法可以是这样:

int sum = 0;
for (int item : list) {
	sum += item;
}

而这就是一个累积的过程。如果用reduce的话:

Optional<Integer> sum = list.stream().reduce((a,b) -> (a + b));

可以看到,虽然集合是List并不是Map,但是入参还是认为有两个,就可以将a认为是sum,b就是item,这样一个持续的累积过程。

而返回值为什么需要是Optional对象,这个Optional也是Java 8的新特性,用于解决NPE问题,详情百度。

而reduce总共有三种写法,第二种也比较常见。我们还是看上面的demo,for循环前定义了变量sum = 0。也就是赋初值。而我们可以直接在reduce方法中给一个初始值:

int sum = list.stream().reduce(10, (a,b) -> (a + b));

但是反之,sum就不需要用Optional来接收了,而其数据类型是跟初始值一样的。

并行流

Stream流分为顺序流和并行流,上篇所写的流就是按照顺序对集合中的元素进行处理——顺序流。而并行流则是使用多线程同时对集合中多个元素进行处理,这样,能够很好的利用当前机器的性能,提高效率,但是同时也可能引发并发安全问题。所以一般认为一部分安全的场景可以使用并行流。

无状态、不干预、关联性。这三大约束确保在并行流上执行操作的结果和在顺序流上执行的结果是相同的。由于不像并发编程那样能手动控制锁,所以并行流尽量在安全的业务场景下使用。

若对集合使用parallelStream()方法,可以为集合创建一个并行流。

Stream<Integer> stream = list.parallelStream();

而我们考虑到并发就是因为在硬件设备到位的情况下,并发执行能很大程度的节省时间。而在并行流上进行的处理,Stream会在底层解决并发问题,保证能够多核执行任务,最后进行合并。所以如果操作集合可能会引发安全问题的情况下,由于并行流的不可控,可能会引发一些并发安全问题。

而在最终,并发的结果是需要进行合并的。如果场景安全,那么其实代码的写法是和顺序流基本一致。

关于业务场景,例如单纯的无顺序数据操作过程,例如:

List<Integer> newList = list.parallelStream().map(item -> item + 10).collect(Collectors.toList());

这样对数据操作基本是没问题的。但是如果是大量数据遍历输出,可以参考并发编程——可能会出现顺序混乱的情况。

而其本质上,是依赖了Fork/Join框架。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/iwts_24/article/details/102868778
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢