1.初步感受一下Stream流
创建流的几种方式
- 1.Collection接口已经提供了stream方法,因此所有的集合都可以使用stream方法创建出来流。
- 2.使用Arrays.stream方法,并将数组传入进去,也可以创建流。
- 3.使用Stream.of或者是Stream.generate可以也可以创建流。
- 4.还有许多常用的数字流,比如IntStream/LongStream以及Random.ints等去创建流。
上面只是做了简单的简介,我们可以通过几个常用的用例来简单使用一下Stream流吧!
案例1 生成1到10的的数组
我们生成1到10的数组的传统做法如下,我们感觉代码有点点复杂对不对...
int N = 10;
int[] arr = new int[N];
for (int i = 0; i < N; i++) {
arr[i] = i + 1;
}
我们有没有什么办法让代码变得简洁?我们完全可以使用到IntStream,使用如下的代码几也可以生成啦!(需要注意的是java中范围一般都是左闭右开,因此实际生成的就是1到10的数)
int[] arr = IntStream.range(1, 11).toArray();
案例2 生成10个随机数
int[] arr = new Random().ints().limit(10).toArray();
需要注意的是,new Random().ints()
创建的是一个无限流,我们这里需要使用limit去限制只产生10个数字。
进阶:产生10个10-100(不含边界)的随机数
我们也可以使用filter限定其范围(不过下面这种方式性能并不高,因为它产生的范围是Integer范围内的,产生出来之后再进行过滤,因此时间比较长)
int[] arr = new Random().ints().filter(i -> i > 10 & i < 100).limit(10).toArray();
案例3 拼接字符串
这也是我们在刷算法时使用ACM模式下经常遇到的问题,在输出时集合元素之间需要加上空格或者是逗号等分隔符,我们使用Stream流就可以让代码变得更加简洁!
String str = Stream.of("my name is wanna".split(" "))
.reduce((s1, s2) -> s1 + "," + s2) //拼接字符串
.orElse(""); //如果为字符串为空的话,设为空串
上面只是简单体验一下,如果不会也没关系,下面我们来对Stream流提供的API进行详细的介绍。
2. 了解Stream流的中间操作和终止操作
中间操作是指用来中间对数据进行过滤、处理等相关的操作。任何中间操作返回的都是一个新的Stream流对象。
终止操作是指调用了这个方法之后,流就结束了,要么输出了/要么返回数据了。
首先我们要知道流是怎么执行的,流它就相当于一个个的过滤器,链式调用和执行,其实就是典型的责任链模式!也就是说它是对于每个数据,链式调用后面的操作,而不是所有的数据一起灌过去的,==第一个数据没被拦截下来/执行到最后是不会执行下一个数据的==。
2.1 流的中间操作
中间又操作包括无状态操作和有状态操作。
什么是有状态操作,什么是无状态操作?无状态操作就是说当前流的执行顺序和当前的数据状态无关,而有状态操作则和当前的数据状态有关,比如sorted必须是排序之后才能进行操作。
一旦遇到有状态的操作就必须等之前过程让每个数据都执行完之后才执行的操作,也就是它会截断之前和之后的操作。
无状态操作包括map/mapToXxx、flatMap/flatMapToXxx、filter、peek、unordered等,有状态操作包括distinct、sorted、limit/skip等。下面对核心的api进行逐一介绍
map-映射
什么是映射呢?就是对流中的所有元素进行统一的操作,比如全部元素乘2等。像极了数学中的函数y=f(x)
,对于所有的x都按照一定的规则f映射到另一个值y。
当我们想要生成2,4,6,8,10这样的一组数,我们就先使用range生成1,2,3,...这样的一组数,再使用map去对每个数进行操作。map操作就是原本是某个类型,操作之后还是该类型,只是进行了某些操作。
int[] arr = IntStream.range(1, 11).map(e -> e * 2).toArray();
如果我们想要1.5,3.0,4.5,...这样一组数呢,原本的类型和我们想要的类型不是同一个类型,这样就可以使用到mapToDouble方法
double[] arr = IntStream.range(1, 11).mapToDouble(e -> e * 1.5).toArray();
如果某些映射我们确实需要转换成别的类型,但是系统并没有提供该方法呢?
我们可以使用它的mapToObj方法。比如我们想要生成字符串的数组,我们使用如下方式并使用toArray得到的Object数组内装的就是你想要的对象,只不过没有类型,需要强转之后再使用(不能直接转成String数组,因为String数组和Object数组没有继承关系)。
Object[] arr = IntStream.range(1, 11).mapToObj(String::valueOf).toArray();
但是如果我们在forEach等方法内进行使用时,对象其实是可以直接使用到本来的类型的,不用进行强转,比如,当然也可以使用Collector去收集数据到一个集合当中(后面会介绍到)。
IntStream.range(1, 11).mapToObj(String::valueOf).forEach(e -> System.out.println(e.length()));
filter-过滤
filter我们接触的比较多,Servlet规范下的Filter就是典型,用来进行过滤,把所有元素进行规律,只有符合断言规则的才需要,不需要的数据直接弃掉。
什么是断言?断言就是定义某个规则,它要求传入的是一个lambda表达式,符合规则return true,不符合规则则return false。
比如我们想要过滤出来所有的偶数,我们就可以使用filter,过滤出来我们需要的数据。下面的例子中返回的就是一个所有的偶数。
int[] arr = IntStream.range(1, 11).filter(e -> (e & 1) == 0).toArray();
flatMap-摊平映射
什么叫摊平呢?比如一个矩阵分为很多行和列,我们可以把每一行提取出来,合并成为一维数组,这样的操作就可以称为摊平。
Arrays.stream("my name is wanna".split(" "))
.filter(e -> e.length() > 2)
.flatMap(e -> e.chars().boxed())
.forEach(e -> System.out.println((char) e.intValue()));
在上面的代码中,我们首先使用空格分割字符串,再使用filter筛选出来长度大于2的部分,然后就剩下name和wanna两个单词了,这两个单词就可以看成矩阵的两个行,我们可以尝试进行摊平,摊平为namewanna
这样一个数组。
flatMap需要返回的也是一个Stream流,e.chars()
返回的是一个IntStream,IntStream并不是Stream的子类,使用boxed进行装箱则是去返回一个Stream<Integer>
。
当然flatMap还提供了toXXX的方法,用来返回的是对应数字类型的流(IntStream/LongStream/DoubleStream等),就不用boxed了,比如下面的代码直接就是转成IntStream。
Arrays.stream("my name is wanna".split(" "))
.filter(e -> e.length() > 2)
.flatMapToInt(String::chars)
.forEach(e -> System.out.println((char) e));
peek
peek主要作用是用于调试操作,我们知道forEach是一个终止操作,可以用于遍历打印/处理等操作,peek的作用和forEach类似,只不过peek是一个中间操作。
IntStream.range(1, 11).peek(System.out::print).filter(i -> (i & 1) == 0).forEach(System.out::print);
上面的代码的执行结果是12234456678891010
,我们可以也可以总结到:流的执行是遍历流当中的每个数据,但是对于每个数据从前往后执行的,如果被filter拦截下来了,那么流的执行就结束了。
limit-用于限制流的执行次数
就以limit为例,用来限制流的执行次数,也可以说成限制流数据的产生个数。
int[] arr = new Random().ints().limit(10).toArray();
sorted-对流数据进行排序
比如我们想要对如下的代码进行执行,它会对所有的元素都执行完filter之前的所有的操作,然后才进行排序,再对流中的每个数据进行打印输出。
Arrays.stream(new int[]{5, 4, 3, 2, 1}).filter(e -> e > 2).sorted().forEach(System.out::print);
假如不加sorted操作呢?
Arrays.stream(new int[]{5, 4, 3, 2, 1}).filter(e -> e > 2).forEach(System.out::print);
它的执行操作是对于5,执行filter,再执行forEach,对于4,先执行forEach,对于3先执行filter,再执行forEach,对于2和1,因为被filter过滤掉了,就不会继续执行forEach了。
从这里我们应该能够体会到有状态操作中的"有状态"究竟是什么意义了!
2.2 流的终止操作
流的终止操作分为短路操作和非短路操作,非短路操作主要包括forEach/forEachOrdered、collect、toArray、reduce、max/mim/count等,而短路操作主要包括findFirst/findMany、allMatch/anyMatch/noneMatch等操作。
什么是短路操作,什么是非短路操作呢?==非短路操作就是它的执行不会阻碍接下来的所有数据的流的执行,而非短路操作的话,如果它匹配了/不匹配了,那么接下来的流中的数据就不会再执行了==。
forEach/forEachOrdered
forEach我们用的比较多,就是遍历执行嘛。forEachOrdered主要是用在并行流当中,用来去保证顺序,在并行流下forEach肯定会乱序的。
IntStream.range(1, 11).parallel().forEachOrdered(System.out::println);
collect-收集
收集操作需要传入一个收集器(Collector),收集器是用来做什么的呢?我们可以用它来将数据收集成为指定的集合类型。
比如我们想要将生成的1-10的数使用列表的方式得到,那么就可以使用collect进行收集,如果需要返回的是数组则可以使用toArray进行收集(不必使用收集器,直接使用Stream流的toArray方法)。
List<Integer> list = IntStream.range(1, 11).boxed().collect(Collectors.toList());
Set<Integer> set = IntStream.range(1, 11).boxed().collect(Collectors.toSet());
收集器工具类Collectors中还提供了一些工具方法的收集器,比如summarizingXxx的方法用来对数据进行汇总,包括最大值/最小值/平均值等信息。
partitioningBy用来给定一个断言,将数据按照符合条件的(TRUE)和不符合条件的(FALSE)分成两组。
groupingBy方法用来进行分组,按照某个字段的信息进行分组,这个和数据库的groupby用法一致。
reduce-缩小
reduce方法接收一个lambda表达式作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。累加器,进行累加时可以是数字的累加,也可以是字符串的累加。
String s = Stream.of("my name is wanna".split(" "))
.filter(e -> e.length() > 2)
.reduce((s1, s2) -> s1 + "|" + s2) //拼接字符串
.orElse(""); //如果为字符串为空的话,设为空串
当然,我们也可以在reduce的第一个参数指定为空的默认值
String s = Stream.of("my name is wanna".split(" "))
.filter(e -> e.length() > 2)
.reduce("", (s1, s2) -> s1 + "|" + s2); //拼接字符串,也可以在第一个参数指定默认值
类似上面的原理,我们也可以编写如下的代码去计算所有的单词的总的长度
Integer length = Stream.of("my name is wanna".split(" ")).map(String::length).reduce(0, Integer::sum);
网友评论