星语课程网
Java8 Stream操作
来源:本站编辑
2021-12-29 11:11
45
1.前言 ==== Java 8的另一大亮点Stream,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。 Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。 Stream API 借助于同样新出现的 [Lambda](https://blog.csdn.net/yy339452689/article/details/110880969) 表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 **fork/join** 并行方式来拆分任务和加速处理过程。 1.1 为什么要用Stream --------------- 有如下几个特点: * 有高效率的并行操作 * 有多中功能性的聚合操作 * 函数式编程,使代码更加简洁,提高编程效率 1.2 什么是聚合操作 ----------- 举个例子,例如我们现在有一个模块的列表需要做如下处理: * 客户每月平均消费金额 * 最昂贵的在售商品 * 本周完成的有效订单(排除了无效的) * 取十个数据样本作为首页推荐 以上这些操作,你可以理解为就是对一个列表集合的**聚合操作**啦,类似于SQL里面的(count()、sum()、avg()....)! 有一些操作,有人可能会说,可以在SQL语句中完成过滤分类!首先不说SQL不能实现的功能,即使SQL能够实现,但是数据库毕竟是用来读写数据的,主要功能是用于数据落地存储的。并不是用来做大量的逻辑处理的,所以不能为了图方便,而忽略了性能方面的损耗!所以,相比之下,有一些列表操作我们必须在程序中做逻辑处理!那如果我们用之前的java处理方式,得像如下操作一样: ```java for(int i=0;i<10;i++){ if(....){ //内部做一系列的逻辑判断处理 }else{ //....... } } ``` 那如果用Stream来处理的话,可能就只有如下简单几行: ```java list.stream().filter().limit(10).foreach(); ``` 所以,代码不仅简洁了,阅读起来也会很是方便! * * * 2.正文 ==== 2.1 Stream操作分类 -------------- **Stream的操作**可以分为两大类:**中间操作、终结操作** **中间操作**可分为: * **无状态(Stateless)操作:**指元素的处理不受之前元素的影响 * **有状态(Stateful)操作:**指该操作只有拿到所有元素之后才能继续下去 **终结操作**可分为: * **短路(Short-circuiting)操作:**指遇到某些符合条件的元素就可以得到最终结果 * **非短路(Unshort-circuiting)操作:**指必须处理完所有元素才能得到最终结果 Stream结合具体操作,大致可分为如下图所示:  2.2 Stream API使用 ---------------- 接下来,我们将按各种类型的操作,对一些常用的功能API进行一一讲解: ### 2.2.1 Stream 构成与创建 **2.2.1.1 流的构成** > 当我们使用一个流的时候,通常包括三个基本步骤: > > 获取一个数据源(source)→ 数据转换 → 执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。 如下图所示:  **2.2.1.2 流的创建** * **通过 `java.util.Collection.stream()` 方法用集合创建流** ```java List
list = Arrays.asList("hello","world","stream"); ` ``` //创建顺序流 ```java Stream
stream = list.stream(); ``` //创建并行流 ```java Stream
parallelStream = list.parallelStream(); ``` * **使用`java.util.Arrays.stream(T[] array)`方法用数组创建流** ```java String[] array = {"h", "e", "l", "l", "o"}; Stream
arrayStream = Arrays.stream(array); ``` * **`Stream`的静态方法:`of()、iterate()、generate()`** ```java Stream
stream1 = Stream.of(1, 2, 3, 4, 5, 6); Stream
stream2 = Stream.iterate(0, (x) -> x + 2).limit(3); stream2.forEach(System.out::println); Stream
stream3 = Stream.generate(Math::random).limit(3); stream3.forEach(System.out::println)` ``` //输出结果如下: 0240.96203191038524260.83036729056585370.09203215202737569 * **`stream`和`parallelStream`的简单区分** **`stream`是顺序流**,由主线程按顺序对流执行操作,而**`parallelStream`是并行流**,内部以多线程并行执行的方式对流进行操作,**需要注意使用并行流的前提是流中的数据处理没有顺序要求(会乱序,即使用了**forEachOrdered**)**。例如筛选集合中的奇数,两者的处理不同之处:  当然,除了直接创建并行流,还可以通过`parallel()`把顺序流转换成并行流: Optional
findFirst = list.stream().parallel().filter(x->x>4).findFirst(); ### 2.2.2 **无状态(Stateless)操作** * **filter**:**筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。** Stream
filter(Predicate super T> predicate); 流程解析图如下:  举个例子: ```java public static void main(String[] args) { List
list = Arrays.asList(6, 7, 3, 8, 1, 2); Stream
stream = list.stream(); stream .filter(x -> x > 5) .forEach(System.out::println); } //结果如下: 678 ``` * **映射(map、flatMap、peek)** **①map:一个元素类型为 T 的流转换成元素类型为 R 的流,这个方法传入一个Function的函数式接口,接收一个泛型T,返回泛型R,map函数的定义,返回的流,表示的泛型是R对象;** **简言之:将集合中的元素A转换成想要得到的B** ```java
Stream
map(Function super T, ? extends R> mapper); ``` 流程解析图如下:  举个例子: ```java //使用的People对象 public class People { private String name; private int age; ...省略get,set方法} //将String转化为People对象 Stream.of("小王:18","小杨:20").map(new Function
() { @Override public People apply(String s) { String[] str = s.split(":"); People people = new People(str[0],Integer.valueOf(str[1])); return people; } }).forEach(people-> System.out.println("people = " + people)); } ``` 或如下(众多姿势,任君选择): List
output = wordList.stream().map(String::toUpperCase).collect(Collectors.toList()); **②flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。** **简言之:与Map功能类似,区别在于将结合A的流转换成B流**
Stream
flatMap(Function super T, ? extends Stream extends R>> mapper) 流程解析图如下:  举个例子: ```java public static void main(String[] args) { List
list1 = Arrays.asList("m,k,l,a", "1,3,5,7"); List
listNew = list1.stream().flatMap(s -> { // 将每个元素转换成一个stream String[] split = s.split(","); Stream
s2 = Arrays.stream(split); return s2; }).collect(Collectors.toList()); System.out.println("处理前的集合:" + list1); System.out.println("处理后的集合:" + listNew); } ``` **③peek:`peek` 操作接收的是一个 `Consumer
` 函数。顾名思义 peek 操作会按照 `Consumer
` 函数提供的逻辑去消费流中的每一个元素,同时有可能改变元素内部的一些属性。** ```java Stream
peek(Consumer super T> action); ``` 这里我们要提一下这个 `Consumer
` ,以理解什么是消费。 `Consumer
` 是一个函数接口。一个抽象方法 `void accept(T t)` 意为接受一个 `T` 类型的参数并将其消费掉。其实消费给我的感觉就是 “用掉” ,自然返回的就是 `void` 。通常“用掉” `T` 的方式为两种: > * **T 本身的 void 方法** 比较典型的就是 `setter` 。 > > * **把 T 交给其它接口(类)的 void 方法进行处理** 比如我们经常用的打印一个对象 `System.out.println(T)` > 操作流程解析图如下:  下面我们来看个例子: ```java Stream
stream = Stream.of("hello", "felord.cn"); stream.peek(System.out::println); ``` 执行之后,控制台并没有输出任何字符串! **这是因为流的生命周期有三个阶段:** * 起始生成阶段。 * 中间操作会逐一获取元素并进行处理。可有可无。**所有中间操作都是惰性的,因此,流在管道中流动之前,任何操作都不会产生任何影响。** * 终端操作。通常分为 **最终的消费** (`foreach` 之类的)和 **归纳** (`collect`)两类。还有重要的一点就是终端操作启动了流在管道中的流动。 所以,上面的代码是因为缺少了终端操作,因此,我们改成如下即可: ```java Stream
stream = Stream.of("hello","felord.cn"); stream.peek(System.out::println) .collect(Collectors.toList()); //控制台打印内容如下:hellofelord.cn ``` **重点:****peek VS map** 最大的区别是: > `peek` 操作 一般用于**不想改变流中元素本身**的类型或者只想元素的内部状态时; > > 而 `map` 则用于**改变流中元素本身类型**,即从元素中派生出另一种类型的操作。 **mapToInt、mapToLong、mapToDouble、flatMapToDouble、flatMapToInt、flatMapToLong** 以上这些操作是**map和flatMap的特例版**,也就是针对特定的数据类型进行映射处理。其对应的方法接口如下: ```java IntStream mapToInt(ToIntFunction super T> mapper); LongStream mapToLong(ToLongFunction super T> mapper); DoubleStream mapToDouble(ToDoubleFunction super T> mapper); IntStream flatMapToInt(Function super T, ? extends IntStream> mapper); LongStream flatMapToLong(Function super T, ? extends LongStream> mapper); DoubleStream flatMapToDouble(Function super T, ? extends DoubleStream> mapper); ``` 此处就不全部单独说明了,取一个操作举例说明一下其用法: ```java Stream
stream = Stream.of("hello", "felord.cn"); stream.mapToInt(s->s.length()) .forEach(System.out::println); //输出结果59 ``` 并且这些指定类型的流,还有另外一些常用的方法,也是很好用的,可以参考:[IntStream](https://docs.oracle.com/javase/8/docs/api/java/util/stream/IntStream.html)、[LongStream](https://docs.oracle.com/javase/8/docs/api/java/util/stream/LongStream.html)、[DoubleStream](https://docs.oracle.com/javase/8/docs/api/java/util/stream/DoubleStream.html) * **无序化(unordered)** **`unordered()`操作不会执行任何操作来显式地对流进行排序。它的作用是消除了流必须保持有序的约束**,从而允许后续操作使用不必考虑排序的优化。 举个例子: ```java public static void main(String[] args) { Stream.of(5, 1, 2, 6, 3, 7, 4) .unordered() .forEach(System.out::println); Stream.of(5, 1, 2, 6, 3, 7,4) .unordered() .parallel() .forEach(System.out::println); } ``` //两次输出结果对比(方便比较,写在一起)第一遍: 第二遍://第一行代码输出 //第一行代码输出5 51 12 26 63 37 74 4 //第二行代码输出 //第二行代码输出3 36 64 77 52 41 15 2 以上结果,可以看到,虽然用了**`unordered()`**`,但是第一个循环里的数据顺序并没有被打乱;是不是很好奇?` 您可以在**[Java 8文档](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#Ordering)**中有一下一段内容: > 对于**顺序流**,顺序的存在与否不会影响性能,只影响确定性。如果流是顺序的,则**在相同的源上重复执行相同的流管道将产生相同的结果**; > > 如果是**非顺序流**,重复执行可能会产生不同的结果。 **对于并行流,放宽排序约束有时可以实现更高效的执行**。 > > 在流有序时, 但用户不特别关心该顺序的情况下,**使用 unordered 明确地对流进行去除有序约束可以改善某些有状态或终端操作的并行性能。** ### 2.2.3 **有状态(Stateful)操作** * **distinct:返回由该流的不同元素组成的流(根据 Object.equals(Object));distinct()使用hashCode()和equals()方法来获取不同的元素。****因此,我们的类必须实现hashCode()和equals()方法。** ```java Stream
distinct(); ``` **简言之:就是去重;**下面看下流程解析图:  举个例子: ```java Stream
stream = Stream.of("1", "3","4","10","4","6","23","3"); stream.distinct().forEach(System.out::println); //输出13410623 ``` 可以发现,重复的数字会被剔除掉!那么如果需要对自定义的对象进行过滤,则需要重写对象的equals方法即可 ! 另外有一个细节可以看到,去重之后还是按照原流中的排序顺序输出的,所以是有序的! * **sorted:返回由该流的元素组成的流,并根据自然顺序排序** 该接口有两种形式:无参和有参数,如: ```java Stream
sorted(); Stream
sorted(Comparator super T> comparator); ``` 那区别其实就在于:**传入比较器的参数,可以自定义这个比较器,即自定义比较规则****。** 举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); stream.sorted().forEach(System.out::println); //输出134891016 ``` * **limit:获取流中n个元素返回的流** 这个很好理解,**和mysql的中的limit函数一样的效果**,返回指定个数的元素流。 ```java Stream
limit(long maxSize); ``` 流程解析图如下:  举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); stream.limit(3).forEach(System.out::println); //输出3110 ``` * **skip:在丢弃流的第一个`n`元素之后,返回由该流的其余元素组成的流。** **简言之:跳过第n个元素,返回其后面的元素流;** ```java Stream
skip(long n); ``` 流程解析图:  举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); stream.skip(3).forEach(System.out::println); //输出16849 ``` ### 2.2.4 **短路(Short-circuiting)操作** * **anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true;** ```java boolean anyMatch(Predicate super T> predicate); ``` 举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); System.out.println("result="+stream.anyMatch(s->s==2)); //输出result=false ``` * **allMatch:Stream 中全部元素符合传入的 predicate,返回 true;* ```java boolean allMatch(Predicate super T> predicate); ``` 举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); System.out.println("result="+stream.allMatch(s->s>=1)); //输出result=true ``` * **noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true.** ```java boolean noneMatch(Predicate super T> predicate); ``` 举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); System.out.println("result="+stream.noneMatch(s -> s>=17 )); //输出result=true ``` * **findFirst:用于返回满足条件的第一个元素(但是该元素是封装在Optional类中)** 关于Optional可以点这里**:[【Java 8系列】Java开发者的判空利器 -- Optional](https://blog.csdn.net/yy339452689/article/details/110670282)** ```java Optional
findFirst(); ``` 举个例子: ```java Stream
stream = Stream.of(3,1,10,16,8,4,9); System.out.println("result="+stream.findFirst().get()); //输出result=3 //当然,我们还可以结合filter处理 System.out.println("result="+stream.filter(s-> s > 3).findFirst().get()); //输出result=10 ``` * **findAny:返回流中的任意元素(但是该元素也是封装在Optional类中)** ```java Optional
findAny(); ``` 举个例子: ```java List
strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya"); String result = strAry .parallelStream() .filter(s->s.startsWith("J")) .findFirst() .get(); System.out.println("result = " + result); //输出result = Jhonny ``` 通过多次执行,我们会发现,其实findAny会每次按顺序返回第一个元素。那这个时候,可能会认为findAny与findFirst方法是一样的效果。**其实不然,findAny()操作,返回的元素是不确定的**,对于同一个列表多次调用findAny()有可能会返回不同的值。使用findAny()是为了更高效的性能。**如果是数据较少,串行地情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个。** 我们接着看下面这个例子: ```java List
strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya"); String result = strAry .parallelStream() .filter(s->s.startsWith("J")) .findAny().get(); System.out.println("result = " + result); //输出result = Jill或result = Julia ``` 如此可见,在并行流里,findAny可就不是只返回第一个元素啦! ### 2.2.5 **非短路(Unshort-circuiting)操作** * **forEach:该方法接收一个Lambda表达式,然后在Stream的每一个元素上执行该表达式** 可以理解为我们平时使用的for循环,但是较于for循环,又略有不同!咱们待会再讲。 ```java void forEach(Consumer super T> action); ``` 举个例子: ```java List
strAry = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya"); strAry.stream() .forEach(s-> { if("Jack".equalsIgnoreCase(s)) System.out.println(s); }); //输出Jack ``` 那如果我们把 "Jack"用在循环外部用一个变量接收,如下操作: ```java String name = "Jack"; strAry.stream() .forEach(s-> { if(name.equalsIgnoreCase(s)) name = "Jackson"; }); ``` 那么此时编辑器则会爆红,  因为lambda中,使用的外部变量必须是最终的,不可变的,所以如果我们想要对其进行修改,那是不可能的!如果必须这么使用,可以将外部变量,移至表达式之中使用才行! * **forEachOrdered:该方法接收一个Lambda表达式,然后按顺序在Stream的每一个元素上执行该表达式** ```java void forEachOrdered(Consumer super T> action); ``` 该功能其实和forEach是很相似的,也是循环操作!那唯一的区别,就在于**forEachOrdered是可以保证循环时元素是按原来的顺序逐个循环的!** **但是,也不尽其然!因为有的时候,forEachOrdered也是不能百分百保证有序!****例如下面这个例子:** ```java Stream.of("AAA,","BBB,","CCC,","DDD,") .parallel() .forEach(System.out::print); System.out.println("\n______________________________________________"); Stream.of("AAA,","BBB,","CCC,","DDD") .parallel() .forEachOrdered(System.out::print); System.out.println("\n______________________________________________"); Stream.of("DDD,","AAA,","BBB,","CCC") .parallel() .forEachOrdered(System.out::print); ``` //输出为: CCC,DDD,BBB,AAA,______________________________________________AAA,BBB,CCC,DDD______________________________________________DDD,AAA,BBB,CCC 可以看到,**在并行流时,由于是多线程处理,其实还是无法保证有序操作的!** * **toArray:返回包含此流元素的数组;当有参数时,则使用提供的`generator`函数分配返回的数组,以及分区执行或调整大小可能需要的任何其他数组** ```java Object [] toArray();
A[] toArray(IntFunction
generator); ``` 举个例子: ```java List
strList = Arrays.asList( "Jhonny", "David", "Jack", "Duke", "Jill","Dany","Julia","Jenish","Divya"); Object [] strAryNoArg = strList.stream().toArray(); String [] strAry = strList.stream().toArray(String[]::new); ``` * **reduce:方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值** 通过字面意思,可能比较难理解是个什么意思?下面我们先看一个图,熟悉一下这个接口的操作流程是怎样的:  该接口含有3种调用方式: ```java Optional
reduce(BinaryOperator
accumulator); T reduce(T identity, BinaryOperator
accumulator);
U reduce(U identity, BiFunction
accumulator, BinaryOperator
combiner); //以及参数的定义结构 @FunctionalInterface public interface BinaryOperator
extends BiFunction
{ //两个静态方法,先进行忽略} @FunctionalInterface public interface BiFunction
{ R apply(T t, U u); //一个默认方法,先进行忽略} ``` 下面举几个例子,看看具体效果: **(一).先以1个参数的接口为例** 为了方便理解,先看下内部的执行效果代码: ```java boolean foundAny = false; T result = null; for (T element : this stream) { if (!foundAny) { foundAny = true; result = element; } else result = accumulator.apply(result, element); } return foundAny ? Optional.of(result) : Optional.empty(); ``` 再看下具体例子: ```java List
num = Arrays.asList(1, 2, 4, 5, 6, 7); Integer integer = num.stream().reduce( new BinaryOperator
() { @Override public Integer apply(Integer a, Integer b) { System.out.println("x:" + a); return a + b; } }).get(); System.out.println("resutl:" + integer); Integer result = num.stream().reduce((x, y) -> { System.out.println("x:" + x); return x + y; }).get(); System.out.println("resutl:" + result); boolean flag = false; int temp = 0; for (Integer integer : num) { if (!flag) { temp = integer; flag = true; } else { System.out.println("x:" + temp); temp += integer; } } System.out.println("resutl:" + temp); ``` x:1x:3x:7x:12x:18resutl:25 **(二)再以2个参数的接口为例** 先看下内部的执行效果代码: ```java T result = identity; for (T element : this stream){ result = accumulator.apply(result, element) } return result; ``` 在看具体例子: ```java List
num = Arrays.asList(1, 2, 4, 5, 6, 7); Integer integer = num.stream().reduce(1, new BinaryOperator
() { @Override public Integer apply(Integer a, Integer b) { System.out.println("a=" + a); return a + b; } }); System.out.println("resutl:" + integer); int temp = 1; for (Integer integer : num) { System.out.println("a=" + temp); temp += integer; } System.out.println("resutl:" + temp); ``` 输出结果都是: a=1a=2a=4a=8a=13a=19resutl:26 **(三)最后3个参数的接口为例** 这个接口的内部执行效果,其实和2个参数的几乎一致。那么第三个参数是啥呢?这是一个combiner组合器; > **组合器需要和累加器的返回类型需要进行兼容,combiner组合器的方法主要用在并行操作中** 在看具体例子: ```java List
num = Arrays.asList(1, 2, 3, 4, 5, 6); List
other = new ArrayList<>(); other.addAll(Arrays.asList(7, 8, 9, 10)); num.stream().reduce(other, (x, y) -> { //第二个参数 System.out.println(JSON.toJSONString(x)); x.add(y); return x; }, (x, y) -> { //第三个参数 System.out.println("并行才会出现:" + JSON.toJSONString(x)); return x; }); ``` //输出结果: [7,8,9,10,1][7,8,9,10,1,2][7,8,9,10,1,2,3][7,8,9,10,1,2,3,4][7,8,9,10,1,2,3,4,5][7,8,9,10,1,2,3,4,5,6] 我们再讲串行流改成并行流,看下会出现什么结果: ```java List
num = Arrays.asList(4, 5, 6); List
other = new ArrayList<>(); other.addAll(Arrays.asList(1, 2, 3)); num.parallelStream().reduce(other, (x, y) -> { //第二个参数 x.add(y); System.out.println(JSON.toJSONString(x)); return x; }, (x, y) -> { //第三个参数 x.addAll(y); System.out.println("结合:" + JSON.toJSONString(x)); return x; }); ``` 我们会发现每个结果都是乱序的,并且多执行几次,都会出现不同的结果。并且第三个参数组合器内的代码也得到了执行!! 这就是因为并行时,使用多线程时顺序性没有保障所产生的结果。通过实践,可以看到:**组合器的作用,其实是对参数2中的各个线程,产生的结果进行了再一遍的归约操作!** 并且仔细看第二遍的执行结果**:每一组都少了一1个值!!!** **所以,对于并行流parallelStream操作,必须慎用!!** * **collect:称为收集器,是一个终端操作,它接收的参数是将流中的元素累积到汇总结果的各种方式** ```java
R collect(Collector super T, A, R> collector);
R collect(Supplier
supplier, BiConsumer
accumulator, BiConsumer
combiner); ``` **第一种方式**会比较经常使用到,也比较方便使用,现在先看一看里面常用的一些方法: **示例:Map
> menuType=Menu.getMenus.stream().collect(partitioningBy(Menu::isType)** **第二种方式**看起来跟reduce的三个入参的方法有点类似,也可以用来实现filter、map等操作! 流程解析图如下:  * * * 3.总结 ==== 此处给正在学习的朋友两个小提示: 1、对于流的各种操作所属分类,还不够熟悉的,可以直接进入方法的源码接口内,如下,是可以查看到类型说明的:  2、对于并行流stream().parallel()、parallelStream()的使用,**须慎重使用**!使用前须考虑其不确定因素和无序性,考虑多线程所带来的复杂性!! 2020年已近年末,再过几天就要步入新年啦!工作之余,耗时几天,终于写完了这篇博文!分享不易,希望感兴趣的朋友,可以留言讨论,点赞收藏!
点赞
热门评论
最新评论
匿名用户
+1
-1
·
回复TA
暂无热门评论
相关推荐
阅读更多资讯
热门评论 最新评论
暂无热门评论