Java-stream(1) Stream基本概念 & Stream接口

Java8 集合中的 Stream 相当于高级版的 Iterator,他可以通过 Lambda 表达式对集合进行各种非常便利、高效的聚合操作(Aggregate Operation),或者大批量数据操作 (Bulk Data Operation)
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返
函数式的解决方案解开了代码细节和业务逻辑的耦合,类似于sql语句,表达的是"要做什么"而不是"如何去做",使程序员可以更加专注于业务逻辑,写出易于理解和维护的代码

一、基本概念

1.1 Stream 操作分类

官方将 Stream 中的操作分为两大类:中间操作(Intermediate operations)和终结操作(Terminal operations)。中间操作只对操作进行了记录,即只会返回一个流,不会进行计算操作,而终结操作是实现了计算操作。

中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响,后者是指该操作只有拿到所有元素之后才能继续下去。

终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作,前者是指遇到某些符合条件的元素就可以得到最终结果,后者是指必须处理完所有元素才能得到最终结果。

操作分类详情如下图所示:在这里插入图片描述
(图片来源:极客时间-《java性能调优实战》第6讲)

我们通常还会将中间操作称为懒操作,也正是由这种懒操作结合终结操作、数据源构成的处理管道(Pipeline),实现了 Stream 的高效

1.2 Stream 主要架构类关系

在这里插入图片描述

二、BaseStream接口

作为stream的父接口,是支持顺序和并行聚合操作的元素序列,控制stream所有类型的行为(包括流操作、流管道、并行操作等)

2.1 接口实现

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

<S>:输入数据类型,输入必须也是stream(S extends BaseStream<T, S>)
<T>:输入数据类型
继承AutoCloseable接口:在流操作结束后会自动执行close()方法去进行一些释放资源的操作

2.2 方法说明

序号方法名入参出参功能说明备注
1unorderedS消除流中必须保持的有序约束,允许之后的操作使用不必考虑有序的优化
2spliteratorSpliterator返回当前流数据的分割迭代器@NotNull
3sequentialS返回一个和入参类型相等的流数据@NotNull
4parallelS将当前流数据改为并行流返回@NotNull
5onCloseRunnable closeHandlerS在调用close方法前调用该方法,按照closeHandler的添加顺序执行。如果执行中有异常的话,将第一个closeHandler抛出的异常转发给close()
6iteratorIterator返回当前流数据的迭代器@NotNull
7isParallelboolean在执行stream的终端操作前调用该方法判断是否执行并行操作
8closevoid关闭stream,调用此流管道的所有关闭处理程序@Override AutoCloseable

2.3 主要继承关系

BaseStream的主要继承关系如下:
在这里插入图片描述
BaseStream下主要有四个实现的子类接口,和一个Pipeline的抽象类实现,这是一个简单的策略模式实现,子类根据自己传入的数据类型不同对某些方法依据自己的特殊要求进行了重写。在stream里,通过对流的形状进行描述来调用不同的实现类(对应的枚举类为StreamShape),从而完成stream根据类型区分的的个性化定制。

StreamShape
REFERENCE对应 Stream,元素为Object实体对象
INT_VALUE对应 IntStream,元素为int类型
LONG_VALUE对应 LongStream,元素为long类型
DOUBLE_VALUE对应 DoubleStream,元素为double类型

三、Stream接口

3.1 概述(接口文档)

什么是stream?

是一个支持串行/并行的聚合操作的元素队列。为了进行计算,组建了一个 stream管道(pipeline),一个流管道由一个源(可能是数组、集合、构造函数、I/O流等)、0到多个中间操作(intermediate operations)和终结操作(terminal operation)组成。

stream有哪些特点?

-》流是惰性的,只有在启动终结操作时才对源数据执行计算,只执行中间操作的时候是不会触发流计算的,并且仅在需要时才使用源元素。

-》集合和stream虽然在表面有一些相似之处,但是他两的侧重点是不一样的。集合主要是对元素的管理和访问。相比之下,stream不提供直接访问或者操作元素的方法,而是关注于声明性地描述元素的聚合和计算操作,然而,如果所提供的流操作不提供所需的功能,则可以使用这些操作来执行受控遍历。

-》如果stram popeline在操作过程中对源数据进行了修改操作会导致不可预知的报错

-》参数会去调用函数接口(Function)或者通常使用lambda表达式引用,所以这些参数必须“非空”

-》在stream中数据从源到终结只会被执行一次,不可以重复操作数据,如果有重复使用的情况会抛出 IllegalStateException 异常(由于一些流操作可能返回其接收器而不是新的流对象,因此可能不可能在所有情况下检测重用)

-》stream有一个close()方法,还有实现了AutoCloseable接口(BaseStream继承),但是大多数stream实例在使用完后不需要关闭,通常,只有源为IO通道的流才需要关闭。大多数流由集合、数组或生成器组成,这些函数不需要特殊的资源管理

-》流管道可以串行执行数据,可以并行执行,通过paralle()方法去选择不同的执行方式

3.2 一些常用方法

filter()

作用:中间-无状态操作,对流数据进行过滤,返回满足入参条件的数据
入参:Predicate<? super T> predicate 数据的过滤条件
出参:Stream:返回一个新的符合条件的Stream 流数据
举例:过滤出 小于3 的元素

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 3, 4).filter(p -> p < 3).collect(Collectors.toList());
  System.out.println(list);
}

输出:[1, 2]

map()

作用:中间-无状态操作,内部通过函数的形式对流数据进行一系列操作,返回函数结果集
入参:Function<? super T, ? extends R> mapper:操作函数
出参:Stream :返回一个新的函数处理结果集的流数据
举例:将每个元素 +1

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 3, 4).map(p -> p + 1).collect(Collectors.toList());
  System.out.println(list);
}

输出:[2, 3, 4, 5]

distinct()

作用:
中间-有状态操作,对当前流数据进行元素去重操作(通过equals()方法判断),返回一个由不同元素组成的流数据,对于有序流,不同元素的选择是稳定的(对于重复的元素,保留遇到的第一个出现的元素),对于无序流,不提供稳定性保证。如果是在并行管道(paralle pipeline)中,保持流数据的排序稳定性是比较昂贵的(要求当前操作充当一个完整的屏障,具有大量的缓冲开销)。

一般情况下业务代码中是不需要保持排序稳定性的,是有无需流数据源(generate())或者通过unordered()方法删除排序约束可以更好的提高在并行管道中的执行效率。如果必须要做到排序的稳定性,最好切换到顺序执行来提高性能。

入参:无入参,对当前流数据进行去重操作(this)
出参:Stream:去重后的一个新的流数据
举例:将元素去重

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 4, 4).distinct().collect(Collectors.toList());
  System.out.println(list);
}
sorted()

作用:中间-有状态操作,对当前流数据进行自然排序(按照compareTo()规则排序),如果当前流数据没有实现Comparable接口的话会抛出 ClassCastException 异常。同样,对于有序流,排序是稳定的。对于无序流,不提供稳定性保证。
入参:无入参,对当前流数据进行排序操作(this)
出参:Stream:排序后的一个新的流数据
备注:该方法还提供了重载方法,入参为Comparator接口对象,这个方法根据Comparator提供的规则来进行排序。
举例:对元素进行排序

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 3, 4, 2).sorted().collect(Collectors.toList());
  System.out.println(list);
}

输出:[1, 2, 3, 4]

peek()

作用:
中间-无状态操作,对流数据进行一些操作,但是它只是对Stream中的元素进行某些操作(比如输出之类),但是操作之后的数据并不返回到Stream中,所以返回的还是原来的元素,不会像map()一样返回一个新的类型的流数据,通常会作为debug打印中间数据使用。

这里要注意的是,如果流数据是个实体对象的话,peek()可以通过调用实体对象的setter方法对其属性值进行改变——也就是说peek()可会流数据进行修改操作,其他方法是不具备的(会创建一个新的对象作为返回结果)。

入参:Consumer<? super T> action:对当前流数据的操作函数
出参:Stream:当前流数据
备注:map()和peek()的区别详解移步:https://www.cnblogs.com/flydean/p/java-8-stream-peek.html
举例:输出每个元素值

public static void main(String[] args) {
  List<Integer> list = Stream.of(1, 2, 3, 4).peek(p -> System.out.println("输出:p = " + p)).collect(Collectors.toList());
  System.out.println(list);
}

输出:

输出:p = 1

输出:p = 2

输出:p = 3

输出:p = 4

[1, 2, 3, 4]

此外stream接口还提供了例如limit(限定流数据的长度),min(返回小元素),max(返回最大元素)等方法,这里就不一一展开说明了,其原理都是相似的。

三、扩展

扩展一:stream的无状态和有状态

《JAVA 8实战》中对流操作的无状态和有状态解释如下:

像 map 和filter这样的操作从输入流中得到每个元素, 并在输出流中产生零个或一个结果。因此, 这些操作通常是无状态的: 它们没有内部状态 (假设用户提供的 lambda 或方法引用没有内部可变状态)。
但是像reduce、sum和max 这样的操作需要有内部状态来累积结果。在这种情况下, 内部状态是小的。在我们的例子中, 它包括了一个 int 或double。无论正在处理的流中有多少元素, 内部状态都是有界大小的。
相比之下, 某些操作 (如sorted或distinct) 首先看起来像filter或map–所有这些都采用流并生成另一个流 (中间操作), 但有一个关键的区别。从流中排序和删除重复项都需要了解以前的历史记录才能完成其工作。例如, 排序要求在将单个项添加到输出流之前对所有元素进行缓冲。操作的存储要求是无限制的。如果数据流是大的或无限的, 这可能是问题。(什么应该逆转所有质数的流?它应该返回最大的质数, 数学告诉我们不存在。我们称这些操作为有状态的操作。

简单来说,就是:

无状态:从输入流中得到每个元素, 并在输出流中产生零个或一个结果,不会有存储数据去维持内部状态,对每个元素处理完成产生结果即可。是无界的,无论多少流数据都可以正常处理。

有状态:从输入流中得到每个元素, 对元素进行运算,但是需要有内部状态来累积结果(比如当前最大值、最小值),无论正在处理的流中有多少元素, 内部状态都是有界大小的,所以是有界的。

扩展二:Predicate接口

Predicate是个断言式接口,其参数是<T,boolean>,也就是给一个参数T,返回boolean类型的结果。跟Function一样,Predicate的具体实现也是根据传入的lambda表达式来决定的。

boolean test(T t);
扩展三:排序稳定性

假定在一个待排序的序列中,存在多个相同的元素,若经过排序操作,这些元素的相对次序不变,则成为这种算法是稳定的,否则就是不稳定的。

eg. 在原序列中,node1==node2 && node1在node2之前
排序后:如node1在node2之前并且无论执行多少次都是这样,则认为这种算法是稳定的,否则是不稳定的。

写在最后 微信公众号【小肖爱吃肉】欢迎关注小肖,日常推送生活
在这里插入图片描述

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页