Stream

(在阅读本文前,请务必掌握Lambda表达式以及方法引用的使用:Lambda表达式——方法引用,Java8新特性Lambda表达式,在本文的示例中,使用了大量的Lambda表达式)。

Stream是Java8中的一大亮点,它与传统的IO流是完全不同的。传统的IO流是在各数据设备节点之间进行传输。而Stream是一种异类的数据操作——聚合操作。

集合是Java中批量数据的常用容器,如果我们要在数据容器中筛选出合适的数据,需要逐一遍历并筛选出符合条件的数据,并将符合的元素放入另一个数据容器中。

聚合操作则不同,首先数据的形式上发生了变化,数据以流的形式存在,每当我们需要进行数据筛选时,会将流中所有数据进行筛选。符合筛选条件的数据会形成一条新的数据流,在Java中,这样的数据流以Stream的形式存在。

1 Stream的特点

Java中的Stream并不能直接存储数据,通常由集合容器或数组转化而来。当我们在Stream上进行聚合操作时(滤器、排序、去重等方式),Stream会根据过聚合操作的内容对数据进行筛选,每次聚合操作都会得到一个新的Stream,筛选的过程被称为中间操作(Intermediate)。

每一个阶段的Stream都可以通过终端操作得到一个结果信息,终端操作将是我们最终获取数据的节点。要注意执行过终端操作的Stream将自动关闭,所以终端的操作也称为最终操作(Terminal)。Stream类型不同,最终操作的手段也不同。如果是数值流,我们可以计算和、平均值等数值操作。

我们把Stream的操作进行总结,可以分为三个部分:1.数据源到Stream的转化,Java8中的集合容器都提供了向Stream转化的功能。2. Stream的聚合操作,聚合操作就是筛选的过程。聚合操作中可以与函数式接口高效的融合,使整个操作更为简洁。3.结果操作,结果操作就是终端操作,将整个Stream计算出一个结果,也可以将Stream再次转化成集合。整个流程如下图所示:

Stream的操作流程

我们已经了解了Stream的特点,下面我们用一个列子来看看Stream的具体操作。在下面示例中,我们定义了类Person,随后我们将多个Person对象放入List中。为了体现出Stream聚合操作的优势,我们用两种方式对集合内的数据进行筛选,并将结果输出。筛选的条件为:1.年龄大于40岁的女性。2.年龄从小到大排序。3.前10个Person对象。

从结果看,两种筛选的方式都达到了目的,我们对比defaultOption和streamOption的实现过程可以发现。streamOption中使用了Stream并配合Lambda表达式进行操作,书写更为简洁。

在Stream的一系列操作中,中间操作返回的类型都是Stream,并且可以链式的执行多次中间操作,每一次中间操作产生的Stream都会被记录,由于中间操作没有产生新的类型或者集合,这些操作也叫做惰性求值方法。

最终操作中我们获取了新的对象,也标志Stream操作的完结,最终操作只能进行一次。由于最终操作会产生新的数据,最终操作方法也称为及早求值方法。

下面我们分别看一下Stream在三个阶段的具体操作。

2 Stream的创建

Stream是不能像普通实例通过new关键子来创建,它的创建方法主要有两种,第一种是利用Stream接口中的静态方法来创建。另外一种是由其它数据集合或者类创建Stream实例。Stream提供的静态方法如下表所示。

Stream中的工厂创建方法

在下面示例中,我们演示了这几种创建Stream方法的使用。

通常情况下,从集合容器转化成Stream是一个比较易行的途径,在实践工作中,如果我们自行实现一个Stream接口并不是一个很好的做法。由于Stream是泛型接口的原因,Stream基本上可以兼容所有类型流的操作。在Java中,还提供了针对int、double、long这三种数据类型的Stream流(IntStream、DoubleStream、LongStream),这些数值流有专用的一些方法可以使用。要了解这些Stream相关类和接口可以在java.util.stream包内进行查看。

除了stream包内的流之外,在Arrays类中,也封装了一系列的stream方法,例如stream(T[] array)以及stream(long[]array)等针对基本数据类型的stream方法。在Random中,也提供了int、long、double的随机数无限长度的Stream方法。这些都是获取Stream流的途径。

3 Stream中间方法

我们说过Stream中间方法都返回一个新的Stream,有些则会转化成其它类型的Stream。这些中间方法可以被划分为两类:一类是无状态中间方法,无状态中间方法在处理元素的时候不会影响到其它元素,例如过滤filter方法。

另一类属于有状态中间方法,有状态中间方法在处理元素的时候需要等待其它元素才能继续进行,如去重、和转化。下面我们列举一些常见的中间方法。

3.1 过滤

filter是Stream中的过滤方法,根据指定的条件进行过滤。过滤后的Stream只包含满足过滤条件的元素。filter方法的参数是一个Predicate函数接口,该函数接口的方法为"boolean test(T t)",t为Stream中的元素。该函数接口的目的就是用来设计一个元素t是否符合要求的算法。在下面示例中,我们设计一个算法,将Stream中非偶数进行过滤。

Stream的中间方法使用了大量的函数接口,所以Stream和Lambda表达式以及方法引用的结合是非常密切的。每一个函数接口都有自身的设计目的,例如filter中的函数接口Predicate常常用于过滤的算法设计。Stream中可能会出现很多陌生的接口,对于这些对象我们一定要紧密的结合API来使用,方便了解它们出现的目的以及如何操作、如何设计算法流程。

3.2 元素转化

map是一个非常重要的转化方法,map方法的参数为Function,Function是一个函数接口,接口方法"R apply(T t)"的目的是将流中的元素t,转化成R类型的元素,由使用者来指定转化算法。map方法可以将Stream流变成Stream流。在下面示例中,我们演示了map方法的使用。

除了map方法外,Stream还有三个固定转化格式的方法mapToDouble、mapToInt、mapToLong,其函数接口的实现的方式也是相似的。

在Stream中还有一个与map比较相似的flatMap,flatMap方法看似与map相似,但内部却截然不同,map是将T类型元素计算并转化成R类型元素,而flatMap是将T类型元素计算并转化成Stream类型元素,并将多个Stream合并成一个Stream。也就是说flatMap可以将原Stream中的元素进行全新的展开计算,最终归纳成一个新的流。如下面示例所示。

Stream也存在flatMapToDouble、flatMapToInt、flatMapToLong这三个数值类型快速转化的方法。

3.3 peek

peek方法用于加工Stream中的元素,map和peek都可以进行数据加工,但peek加工后,不会改变数据的类型。peek方法的参数是Consumer,这个接口是我们比较熟悉的一个函数接口,forEach方法中的参数就是这个接口对象。Consumer它是一个消费接口,消费接口方法"void accept(T t)"的目的是处理和使用元素。通过这个接口过程我们使用Stream中的参数执行各种操作,有可能会修改Stream中元素的内容。在下面示例中,我们演示peek方法的使用。

peek一般用于在某一个节点时查看Stream中的元素,常常用于数据的检测。

3.4 元素截取

元素截取的方法有skip和limit,它们的作用是限制Stream中元素个数的。方法skip(long n)丢弃流中的前n个元素,返回剩余的流,如果此流中的元素少于n个,则返回一个空Stream。skip方法是一个有状态的中间方法。

方法limit(long maxSize)用于表示将当前的流截断,返回一个最多含有maxSize个元素的Stream。limit方法比较特殊,它虽然是一个有状态中间方法,但它同时也是一个短路状态中间方法。短路操作我们并不陌生(例如运算符"&&"和"||"的短路操作),在Stream中,短路操作一旦成立,后续的元素不会被处理。在下面示例中,我们演示了skip和limit方法的操作。

示例中的Stream经过flatMap的转化,变成了一个无限有序的斐波那契数列。为了验证skip和limit的有效性,我们先将斐波那契数列的前10位进行输出(行17到20)。然后从Stream中截取数列的一部分进行比对(取前10位的后5位数列)。通过观察结果我们可以看到skip和limit的截取是正确的。

3.5 有序操作

Stream中的元素有序操作有两个方法,分别是distinct和sorted,这二者都是有状态的中间操作。

distinct方法对重复元素只保留一个,它的验证元素是否重复使用Object的equals方法进行验证。如果Stream中的元素具备自然排序的能力,可以使用sorted方法进行排序,排序后的Stream是有序的,如果Stream中的元素不具备自然排序的能力,调用该方法会抛出ClassCastException异常。

如不支持自然排序,可使用sorted(Comparator super T> comparator)重载方法进行排序。有序操作如下面示例所示。

4 Stream最终方法

最终方法是从Stream中获取最终结果的方法操作,一旦执行了最终操作,意味Stream的工作流程结束,Stream将不能在去执行中间方法。Stream的最终方法分为短路操作和非短路操作,短路操作不用处理完所有的元素就可以返回结果,而非短路操作必须处理完所有元素才能返回结果。我们熟悉的Stream.forEach方法就属于一个最终方法,下面我们对Stream中的常用最终方法进行介绍。

4.1 统计方法

Stream中的常用统计方法有三个,分别是count、max、min。count方法返回Stream中元素的个数计数。max和min方法返回Stream中最大的元素和最小的元素。max和min方法需要我们提供一个比较器用于元素之间的比较。在下面示例中,我们演示了这三个方法的使用。

上述示例中,只有count方法返回的是一个长整类型值,max和min返回的是一个Optional对象。Optional是一个泛型容器类,与Collection和Map不同的是,Optional是一个只包含一个元素的容器。Optional对象中可能没有元素,可以通过实例方法isPresent()来进行判断,如果存在元素值可以使用get()方法来获取。此外Optional也存在与Stream类似的转化和过滤方法等。在示例中,max和min在返回Optional对象后,我们直接用get方法获取了元素值(行16、17)。

4.2 短路匹配

Stream提供了三个短路匹配方法,分别是allMatch、anyMatch、noneMatch。这三个方法分别验证Stream中的元素是否全部匹配、任意一个匹配以及没有匹配,返回结果是一个boolean值。这三个方法的参数是一样的,都是函数接口Predicate,也正是filter方法参数使用的接口对象。我们正是利用Predicate函数接口来制定匹配规则,如下面示例所示。

除了短路匹配操作外,还有两个短路查找操作分别是findAny(获取流中的某一个元素)和findFirst(获取流中的第一个元素),这两个方法返回的是容器Optional。这两个方法的具体使用我们不做演示。

4.3 收集操作collect

collect操作是将Stream中的元素收集到目标容器中,常常用于Stream向目标容器进行转化而使用。Stream在收集转化的过程中,还可以对Stream中的元素经过加工,将最后加工的数据收集到目标容器中。Stream的collect方法有两个重载方法,分别如下:

1. R collect(Collector super T,A,R> collector)

2. R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)

从第一个方法来看,collect需要提供一个Collector接口对象(收集者接口),这说明第一个collect方法实际上是一个针对Collector的实现,我们先暂时不理会该方法,我们先看第二个方法。

在第二个collect方法中的三个参数,分别是Supplier函数接口类型的supplier,以及两个BiConsumer函数接口类型的accumulator和combiner。它们起到的作用和功能如下:

1.Supplier supplie:该函数接口的参数类型"R"与collect返回的类型是一致的,方法"T get()"的目的就是返回一个新对象,如果我们想将Stream转化成List,get方法内应该返回一个List实例(如果想将Stream转化成String,get方法返回一个String实例)。一般来说,该方法是构造方法引用的最佳实践,如"ArrayList::new"等。

2.BiConsumer accumulator:该函数接口参数类型是二元组泛型,其中"R"表示返回类型,"T"表示Stream的元素类型。接口方法"accept(R r, T t)"的目的是建立目标容器与Stream元素之间的收集关系。例如我们要将Stream转化成List,则accept方法内部应该是将Stream中的元素加入到List中。

3. BiConsumer combiner:该函数接口的目的是将多个容器内的元素叠加到一起。

在单线程的情况下,参数supplier和accumulator就可以实现collect到目标容器的收集转化操作。最后一个参数combiner起到了多个容器累加的作用,一般情况下我们创建或者获取的Stream是一个顺序流,该参数是没有效果的,我们是可以忽略该参数的。如果Stream是一个通过集合类的parallelStream方法生成的并行流,则该参数是有效的。

在下面示例中,我们演示如何将Stream转化成List或者String。

从collect方法的使用上来看,Stream到目标类型的转化过程中,最重要的转化实现是与第二个参数accumulat息息相关。

我们甚至可以利用它实现filter和map的功能。如下面示例所示。

我们之前的Stream默认为顺序流(单线程梳理流),collect方法的第三个参数combiner并没有实际的意义,但在并行流的环境下,该参数必须实现容器累加的算法,否则最终收集的元素的容器会出现丢失元素的情况,如下面示例所示。

在上述示例的环境下(并行流),combiner是必须实现的,否则Stream中只有部分元素会被收集到List中。并行流在进行数据处理的时候,采用多线程机制,当数据量大的时候,能够体现出速度上的优势。

collect的另一个重载方法,需要一个Collector接口对象,Collector是一个收集器,Collector并不是一个函数接口,它是一个由多个函数接口构成的聚合接口,它包含的接口和含义如下:

1.Supplier函数接口,由它创建一个结果容器,类型A不一定是最终类型,有可能是中间类型。

2.BiConsume函数接口,它将新元素合并到结果容器中。将Stream中T类型元素添加至最终类型A容器中。

3.BinaryOperator函数接口,该接口是BiFunction的子接口,它是一个组合器,将两个结果合成一个。

4.Function函数接口最终转化器,将A转化成R,R为最终返回类型。

除此之外Collector还需要一个无固定参数的Collector.Characteristics... characteristics,Collector.Characteristics是一个枚举,用来描述Collector的特征,三个枚举常量如下:

1.CONCURRENT表示容器是并发的,支持多个线程容器相同结果的累加、

2.IDENTITY_FINIS功能标识,可以省略。

3.HUNORDERED集合操作不保证保留元素的顺序。

在创建Collector时,Function接口并不一定是必选项,如果Supplier提供的就是最终类型,我们完全可以在BiConsume接口中完成元素添加的操作。另外如果包含多个该枚举,我们可以使用一个Set枚举集合来表示。在下面示例中,我们使用Collector的方式实现Stream向List的转化。

一般情况下,我们是不会直接去实现Collector接口对象,而是利用Stream体系中提供的Collectors工具类来完成。如使用"Collectors.toList()"方法代替Collector对象,一样可以完成Stream到List的转化。Collectors工具类中的内容我们不做详细讲解,它提供了向Set、Map、List的转化,以及一些计算统计等实现。

4.4 归纳操作reduce

reduce方法也称归纳方法,它可以将Stream中的元素归纳成一个对象,在归纳的过程中也可以对Stream中的元素进行操作,从方法描述上来看,reduce是可以代替collect方法。Stream中的reduce方法共有三个重载方法,分别如下:

1.reduce(BinaryOperator accumulator)

2.reduce(T identity, BinaryOperator accumulator)

3.reduce(U identity, BiFunction accumulator, BinaryOperator combiner)

reduce(BinaryOperator accumulator)方法返回的是单数据容器Optional,BinaryOperator函数接口是用来做元素之间的运算。在讲述Collector的时候,我们曾经也接触过该接口,它是一个子类接口,它的目的是将两个同类型的元素组合成一个同类型的新值。在reduce中,BinaryOperator也起到了对Stream中元素进行累加的作用,我们通过一个示例来了解一下reduce(BinaryOperator accumulator)的作用:

从上述示例的运行结果中,我们可以看到reduce方法的BinaryOperator的接口方法"apply(result,element)"的效果,当开始寻找元素时,形参result为Stream中的第一个元素,形参element为下一个元素。在随后的过程中,形参result为上一次apply(result,element)的结果,element为下一个Stream中寻找的元素。

利用这个特点,我们可以完成Stream中,最大、最小元素的获取操作,以及一些数据的计算操作,例如在上述示例中,我们完成了Stream流中各元素的求和计算操作,但该重载方法不适合计数计算以及收集转化。适合元素之间的运算和比较操作。

reduce的第二个方法包含了两个参数,比一各参数的reduce方法多了一个identity,该参数不是Stream中的元素,是一个额外基数。在接口方法"apply(result,element)"中,形参result的初始值为identity,如下面示例所示。

identity的作用就是给"apply(result,element)"中的result提供一个初始值,除此之外,它与一个参数的reduce方法无差别。

上述的两个reduce方法有个致命的缺陷,涉及到的所有参数类型必须与Stream中参数类型一致,这也导致reduce在计算的过程中,无法得到额外的类型,计算手段也受限。而第三种reduce方法就可以解决这个问题。

reduce的第三个方法包含了三个参数,分别是U identity, BiFunction accumulator以及BinaryOperator combiner。identity同样也是一个额外基数,BiFunction用于累加器,它的作用与reduce第二个方法中的BinaryOperator参数的作用是一样的,不同的是BiFunction可以指定额外的结果类型,该类型是reduce最终的返回类型。最后一个参数combiner起到的作用也是容器累加的作用,在并行流中具有意义。在下面示例中,我们演示reduce方法将Stream转化成List(替代collect方法)。

在上述示例中,reduce的第一个参数identity的类型,就是reduce归纳的结果类型,当用reduce代替collect的时候,identity起到了Supplier接口的作用。第二个参数accumulator起到了归纳(收集)合并的作用,上述示例在归纳合并的过程中,将Stream中为偶数的元素放入集合中。

5 Stream中相关的函数接口

在Stream中,Stream中出现了大量的函数接口,这些接口承载了Stream各种功能的实现,如果我们不熟悉这些函数接口,在操作Stream的时候就会遇到不少麻烦。在本小节中,我们将Stream中出现的接口再次列举出来,希望可以帮助大家更好的理解和使用Stream中的方法,这些接口见下表。

Stream中用到的函数接口

在这些接口中,只有Comparator接口的目的意义比较明确,其余接口的方法实现,完全由开发者自由发挥。在Stream的环境下,可以实现不同的算法。在Stream中,还有一个重要的接口Collector,是一个由多个函数接口组合成的复合接口,在使用该接口时,我们只要明白其成员接口所承担的任务就算完成对Collector的理解,如果没有特殊要求尽量不要去实现或者创建一个Collector接口,尽量使用Collectors类来获取Collector接口实例。

java 8 新特性_Java8新特性Stream相关推荐

  1. java的collect用法_java8新特性:stream流中collect用法

    java8新特性:stream流中collect用法 java8新特性:stream流中collect用法 1.toList List collectList = Stream.of(1, 2, 3, ...

  2. java打印日期序列_Java8新特性之新日期API

    早期的日期 API 在早期也就是 Java 8 之前,JDK 原生比较有名的有两个类: Date 类 Calendar 类 这两个类相对来说用起来是比较困难的,之前我们往往是用的第三方的库. 新日期 ...

  3. java新应用_java8新特性的实际应用

    Java8自从14年发布之后增加了很多新特性,其中好多特性在实际应用中都可以用到,不仅简化了我们代码,还弥补了旧版本里的一些不足.这里只列举出一些实际开发中可能用得到的特性,加以说明. 函数式接口 函 ...

  4. java8的stream特性_Java8新特性介绍:Stream API

    Stream API 了解Stream Java8中有两个比较大的改变 Lambda表达式 Stream API (java.util.stream.*) Stream是Java8中处理集合的关键抽象 ...

  5. java 8 详解_java8新特性详解(转载)

    1.Lambda演变过程 @Data @ToString @NoArgsConstructor @AllArgsConstructorpublic classStudent {//名字 private ...

  6. java 接口的静态方法_Java8新特性:接口的默认方法与接口的静态方法

    默认方法允许接口方法定义默认实现,子类方法不必须实现此方法而就可以拥有该方法及实现.如下: public interface DefaultFuncInter { int getInt(); defa ...

  7. java lamdba表达式效率_java8新特性Lambda表达式为什么运行效率低

    Lambda表达式为什么运行效率低 准备 我为什么说Lambda表达式运行效率低. 先准备一个list: 先用Lambda表达式的方式来循环一下这个list: 运行时间大概为110ms 再用普通方式来 ...

  8. java三目表达式_Java8新特性Lambda表达式

    1 Lambda表达式 对于很多计算机语言来说,Lambda表达式并不是陌生的语法格式,而对于Java而言,它的到来比较晚,直到Java8更新之后,Lambda表达式才正式出现在Java语法中.所以如 ...

  9. java8新特性_Java8新特性之Date API|乐字节

    大家好,我是乐字节的小乐,上篇文章讲述了<Java8新特性之Optional>,接下来,小乐将接着讲述Java8新特性之Date API 2019日历 Java8之Date API Jav ...

最新文章

  1. pandas 修改数据和数据类型
  2. String StringBuilder StringBuffer 对比 总结得非常好
  3. python win10还是linux_在win10的Linux子系统(WSL)上搭载python编程环境
  4. DotNet 网上相关资源
  5. 6大设计原则之开闭原则
  6. ​如何让技术想法更容易被理解?
  7. 循环小数是分数集合吗_人教版小学数学三年级上册 分数的简单计算 教案、课件,公开课视频...
  8. [转载] JAVA中分为基本数据类型及引用数据类型
  9. vba 自动排序_Excel表格自动排序,神了!
  10. [转] VR-FORCES 介绍
  11. 高通 mdm9607编译以及audio框架
  12. prometheus安装使用
  13. linux分区修复命令,在Linux下成功修复分区表出错
  14. 网络抖动多少ms算正常_网络延迟多少ms才算正常
  15. 免费的静态网页托管_如何使用自动管道免费托管静态站点
  16. 8K播放网络全终端播放器H5播放器网页直播/点播播放器EasyPlayer和vlc播放RTSP流地址不兼容问题排查解决
  17. Python开发mysql和mongo 连接类
  18. 通达信接口官网与量化交易有联系吗?
  19. [重要新功能]删除自己发表的评论
  20. Ubuntu 局域网通信工具之信使(iptux)安装及自动隐藏的解决方法

热门文章

  1. 计算机实用教程pdf,计算机基础实用教程电子版.pdf
  2. 单位意义:dB、dBm与dBw、dBμ与dBV、dBi与dBd、dBFS
  3. 关于flink cdc 抽取oracle数据 oracle表名大小写的问题
  4. c8051f340当io脚设为数字输入时设置需要注意
  5. 智慧穿戴装置带动医疗大数据发展
  6. 【调剂】计算机好的矿山安全、计算机等专业工科男(学硕)调剂信息
  7. 科技新品 | 卡西欧高性能金属腕表;佳能大幅面打印机;三星2亿像素传感器
  8. ps----制作双眼皮
  9. 使用SystemParametersInfo函数实现更改计算机桌面背景图片
  10. 用 Prettier 美化代码