上一篇:java8特性 stream笔记(基础部分)
https://blog.csdn.net/Ding_Creator/article/details/117523051
流的工作过程
流的工作过程
创建流->中间操作->终止操作
其中中间操作可以有0个至多个,终止操作只能有一个
/**
* 如果有多个终止操作
*/
class DuplicateTerminal {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(2, 2);
Stream<Integer> stream = list.stream();
stream.collect(Collectors.toList());
stream.forEach(System.out::println);
}
}
输出结果:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at DuplicateTerminal.main(Show.java:57)
Process finished with exit code 1
这里也有一个小问题,创建流的操作非常容易识别,最常用的list.stream()就是创建流的操作,那么怎么区分中间操作和终止操作呢?
其实也很简单,当使用的方法返回一个Stream的对象,那么就是中间操作,反之则是终止操作
在开始详细讲解流的工作过程之前,先要明确四个特点:
1.Stream自己不会存储元素。
2.Stream的操作不会改变源对象。相反,他们会返回一个新Stream
3.流是一次性的,每个流只能使用一次。
/**
* 实际上,已被操作的流不能被再次操作,流是一次性的
*/
class DuplicateStream {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
Stream<Integer> stream = list.stream().filter(t->t>1);
stream.filter(t -> t > 3);
stream.filter(t -> t > 2);
}
}
输出结果:
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
at java.util.stream.ReferencePipeline$2.<init>(ReferencePipeline.java:163)
at java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:162)
at DuplicateStream.main(Show.java:69)
Process finished with exit code 1
4.Stream 操作是延迟执行的。它会等到需要结果的时候才执行。也就是执行终止操作的时候。也就是说,如果没有终止操作,那么流不会执行任何操作
/**
* 无终止操作
*/
class NoTerminal {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(2, 3, 4);
list.stream().filter(t -> {
System.out.println(t);
return t > 2;
}).map(t -> {
t = t + 2;
System.out.println(t);
return t;
});
}
}
输出结果:
Process finished with exit code 0
现在开始正式讲解流的工作流程
1.创建流
1.1 使用Collection中的stream()或者parallelStream()方法
List<Integer> list = new ArrayList<>();
// 创建串行流
list.stream();
// 创建并行流
list.parallelStream();
1.2 使用Arrays中的stream()方法
Integer[] array = {1, 2, 3, 4, 5};
// 第一种使用方式
Arrays.stream(array);
// 第二种使用方式,两个参数为两个下标,左闭右开区间
Stream<Integer> stream = Arrays.stream(array, 2, 4);
//打印一下看看结果
stream.forEach(System.out::println);
输出结果:
3
4
Process finished with exit code 0
1.3 Stream中的静态方法
Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 2).limit(6);
Stream<Double> stream3 = Stream.generate(Math::random).limit(2);
这里有一个很有意思的点,如果使用后面两个方法不加上limit(),那么生成的将是一个无限流,那么垃圾回收是否会回收无限流产生的数据呢?
/**
* 无限流(垃圾回收是否会回收这些数据)
*/
class UnlimitedStream1 {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().maxMemory());
Stream.generate(() -> {
double d = Double.MAX_VALUE;
System.out.println("number=" + d);
return d;
})
// .forEach(System.out::println);
.collect(Collectors.toList());
}
}
设置启动参数以便更快看到效果
-Xms1m -Xmx1m
你会发现当使用forEach()时,并不会发生内存溢出的错误,但是当你使用的是collect(Collectors.toList())时,会发生内存溢出,其实这是因为当你调用collect()方法时,会将这些生成的数据收集起来,当数据量到一定程度,就溢出了,而使用forEach()方法时,每个数据走了一遍forEach()后就不再被使用,然后就被回收了
1.4 使用 BufferedReader.lines() 方法,将每行内容转成流
BufferedReader reader = new BufferedReader(new FileReader("文件路径"));
Stream<String> lineStream = reader.lines();
1.5 使用 Pattern.splitAsStream() 方法,将字符串分隔成流
Pattern pattern = Pattern.compile(",");
Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
2.中间操作
中间操作包括:
无状态(Stateless)操作:每个数据的处理是独立的,不会影响或依赖之前的数据。如
filter()、flatMap()、flatMapToDouble()、flatMapToInt()、flatMapToLong()、map()、mapToDouble()、mapToInt()、mapToLong()、peek()、unordered() 等
有状态(Stateful)操作:处理时会记录状态,比如处理了几个。后面元素的处理会依赖前面记录的状态,或者拿到所有元素才能继续下去。如distinct()、sorted()、sorted(comparator)、limit()、skip() 等
接下来所有例子中用到的Student类都是这一个
class Student implements Comparable<Student> {
private Long number;
private Integer age;
public Student(Long number) {
this.number = number;
}
public Student(Long number, Integer age) {
this.number = number;
this.age = age;
}
public Long getNumber() {
return number;
}
public void setNumber(Long number) {
this.number = number;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"number=" + number +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
if (o.getNumber() > this.getNumber()) {
return -1;
}
return 1;
}
}
下面来看看有状态和无状态的区别
class StatefulAndStateless {
public static void main(String[] args) {
Student s1 = new Student(101L, 10);
Student s2 = new Student(102L, 12);
Student s3 = new Student(100L, 8);
Student s4 = new Student(104L, 9);
List<Student> studentList = Arrays.asList(s1, s2, s3, s4);
studentList.stream().map(t -> {
System.out.println("开始排序");
return t;
}).forEach(t -> System.out.println("收集"));
System.out.println("=======================");
studentList.stream().sorted((o1, o2) -> {
System.out.println("开始排序");
return o1.compareTo(o2);
}
).forEach(t -> System.out.println("收集"));
}
}
运行结果:
开始排序
收集
开始排序
收集
开始排序
收集
开始排序
收集
=======================
开始排序
开始排序
开始排序
开始排序
开始排序
开始排序
收集
收集
收集
收集
很神奇吧,这两个操作打印出来的结果完全不一样。交叉打印的,说明后续操作不需要等前一操作全部完成即可进行,是无状态操作。而另一种这说明需要等前一步骤完成,才能进行下一步操作。这就是有状态和无状态操作的区别。
接下来介绍几个中间操作的用法
2.1 筛选与切片
筛选:用一个返回boolean的函数筛选,筛选出各不相同的元素
filter:通过设置的条件过滤流中的某些元素
distinct:通过流中元素的 hashCode() 和 equals() 去除重复元素
切片:忽略流中的头几个元素或截取流中的前几个元素
limit(n):获取n个元素
skip(n):跳过n元素,配合limit(n)可实现分页
List<Integer> list = Arrays.asList(2, 2, 3, 4, 1);
list.stream().filter(t -> t > 2).forEach(t -> System.out.print(t + ","));
System.out.println("\n============================");
list.stream().limit(2).forEach(t -> System.out.print(t + ","));
System.out.println("\n============================");
list.stream().skip(2).forEach(t -> System.out.print(t + ","));
System.out.println("\n============================");
list.stream().distinct().forEach(t -> System.out.print(t + ","));
输出结果:
3,4,
============================
2,2,
============================
3,4,1,
============================
2,3,4,1,
2.2 映射
map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
List<Integer> list1 = Arrays.asList(2, 2, 3, 4);
list1.stream().map(t -> t * 2).forEach(System.out::println);
输出结果
4
4
6
8
flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。(注意观察,跟map并不相同)
List<String> list2 = Arrays.asList("a,b,c", "1,2,3");
List<String> flatMapList = list2.stream().flatMap(t -> {
Stream<String> stream = Arrays.stream(t.split(","));
System.out.println("stream=" + stream);
return stream;
}).collect(Collectors.toList());
System.out.println("======================");
System.out.println("flatMapList=" + flatMapList + ",size=" + flatMapList.size());
输出结果
stream=java.util.stream.ReferencePipeline$Head@27bc2616
stream=java.util.stream.ReferencePipeline$Head@3941a79c
======================
flatMapList=[a, b, c, 1, 2, 3],size=6
如果是map,中间过程打印出来两个流,那么collect后输出的结果也会是两个流,但是flatMap则是把两个流合成了一整个流,所以打印出来的是六个元素(如果不信,可以试试map,看看打出来的结果)
2.3 排序
sorted():自然排序,流中元素需实现Comparable接口
sorted(Comparator com):定制排序,自定义Comparator排序器
Student s1 = new Student(101L, 10);
Student s2 = new Student(102L, 12);
Student s3 = new Student(100L, 8);
Student s4 = new Student(104L, 9);
List<Student> studentList = Arrays.asList(s1, s2, s3, s4);
//默认排序(使用的排序方法为student实现compare接口时重写的方法)
studentList.stream().sorted().forEach(System.out::println);
System.out.println("==================");
//自定义排序:先按年龄升序,年龄相同则按学号升序
studentList.stream().sorted((o1, o2) -> {
if (o1.getAge().equals(o2.getAge())) {
return o1.getAge() - o2.getAge();
} else {
return o1.getAge().compareTo(o2.getAge());
}
}).forEach(System.out::println);
输出结果
Student{number=100, age=8}
Student{number=101, age=10}
Student{number=102, age=12}
Student{number=104, age=9}
==================
Student{number=100, age=8}
Student{number=104, age=9}
Student{number=101, age=10}
Student{number=102, age=12}
2.4 消费
peek:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。
List<Integer> list1 = Arrays.asList(2, 2, 3, 4);
list1.stream().peek(t -> {
t = t * 2;
System.out.println("peek: t=" + t);
}).forEach(System.out::println);
System.out.println("=======================");
list1.stream().map(t -> {
t = t * 2;
System.out.println("map: t=" + t);
return t;
}).forEach(System.out::println);
输出结果
peek: t=4
2
peek: t=4
2
peek: t=6
3
peek: t=8
4
=======================
map: t=4
4
map: t=4
4
map: t=6
6
map: t=8
8
可以看到,map中对元素的变化有影响到后续操作,而peek则没有
3.终止操作
终止操作包括:
非短路操作:处理完所有数据才能得到结果。如
collect()、count()、forEach()、forEachOrdered()、max()、min()、reduce()、toArray()等。
短路(short-circuiting)操作:拿到符合预期的结果就会停下来,不一定会处理完所有数据。如anyMatch()、allMatch()、noneMatch()、findFirst()、findAny() 等。
我们用一个例子来对比两者的区别
/**
* 短路与非短路
*/
class ShortCircuit {
public static void main(String[] args) {
List<Integer> list1 = Arrays.asList(4, 6, 1, 3, 6, 6, 2, 4, 1, 8, 9, 7);
list1.stream().peek(t -> System.out.println("peek:" + t)).anyMatch(t -> {
System.out.print(t + ",");
return t > 3;
});
System.out.println("\n============================");
int max = list1.stream().peek(t -> System.out.print(t + ",")).max(Integer::compareTo).get();
System.out.println("max=" + max);
}
}
输出结果
peek:4
4,
============================
4,6,1,3,6,6,2,4,1,8,9,7,max=9
可以发现,当短路操作遇到第一个不符合条件的元素会立即停止,这个停止不仅包括终止操作,连中间操作的后续也被停止了。而非短路操作则会走完整个流程。
接下来介绍几个终止操作的用法
3.1 匹配、聚合操作
allMatch:接收一个 Predicate 函数,当流中每个元素都符合该断言时才返回true,否则返回false
noneMatch:接收一个 Predicate 函数,当流中每个元素都不符合该断言时才返回true,否则返回false
anyMatch:接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
findFirst:返回流中第一个元素
findAny:返回流中的任意元素
count:返回流中元素的总个数
max:返回流中元素最大值
min:返回流中元素最小值
/**
* 匹配、聚合操作
*/
class TerminalStream1 {
public static void main(String[] args) {
List<Integer> list1 = Arrays.asList(4, 6, 1, 3, 6, 6, 2, 4, 1, 8, 9, 7);
boolean allMatchResult = list1.stream().allMatch(t -> {
System.out.print(t + ",");
return t > 3;
});
System.out.println("allMatchResult=" + allMatchResult);
boolean anyMatchResult = list1.stream().anyMatch(t -> {
System.out.print(t + ",");
return t > 3;
});
System.out.println("anyMatchResult=" + anyMatchResult);
}
}
输出结果
4,6,1,allMatchResult=false
4,anyMatchResult=true
3.2 规约操作
Optional reduce(BinaryOperator accumulator):第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
T reduce(T identity, BinaryOperator accumulator):流程跟上面一样,只是第一次执行时,accumulator函数的第一个参数为identity,而第二个参数为流中的第一个元素。
U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner):在串行流(stream)中,该方法跟第二个方法一样,即第三个参数combiner不会起作用。在并行流(parallelStream)中,我们知道流被fork join出多个线程进行执行,此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样,而第三个参数combiner函数,则是将每个线程的执行结果当成一个新的流,然后使用第一个方法reduce(accumulator)流程进行规约。
那么问题来了,如果流中没有元素或者只有一个元素会发生什么呢?
List<Integer> emptyList = new ArrayList<>();
emptyList.stream().peek(System.out::println).reduce(Integer::sum);
emptyList.add(1);
emptyList.stream().peek(System.out::println).reduce(Integer::sum);
输出结果
1
好像无事发生,但当我们尝试去get
List<Integer> emptyList = new ArrayList<>();
emptyList.stream().peek(System.out::println).reduce(Integer::sum).get();
输出结果
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.util.Optional.get(Optional.java:135)
at ReduceStream.main(Show.java:359)
这也就说明,我们对于返回的Optional对象,在get之前必须通过isPresent()方法去判断是否有值
那么接下来具体讲讲reduce()方法
第一个和第二个reduce比较好理解,那么第三个reduce是什么效果呢?我们可以看下例子
/**
* 规约操作
*/
class ReduceStream {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int reduce1 = list.stream().reduce(Integer::sum).get();
int reduce2 = list.stream().reduce(5, Integer::sum);
int reduce3 = list.stream().reduce(5, Integer::sum, Integer::sum);
int reduceP3 = list.stream().parallel().reduce(5, Integer::sum, Integer::sum);
System.out.println("reduce1:" + reduce1 + ", reduce2:" + reduce2 + ", reduce3:" + reduce3 + ", reduceP3:" + reduceP3);
}
}
输出结果
reduce1:10, reduce2:15, reduce3:15, reduceP3:30
可见在并行流下第三个reduce计算出的结果和串行流是不一致的,这个要特别注意。这是因为这四个元素被分配到了四个任务中,每个任务计算的时候都有一个初始值5,这样合并的时候就会比串行流多出来3个5,就变成了30。大家可以试下换几个值验证一下,也可以更改第二个参数和第三个参数中的重写方法去验证
3.3 收集操作
顾名思义,收集操作就是将所有元素收集起来。
我们先捋一遍收集操作需要哪些步骤。在串行流中,第一步,肯定是要有一个容器容纳这些元素。第二步,需要把处理好的元素放入这个容器。第三步,如果类型发生了变化,需要转型。
而在并行流中,还有一个将各个任务的结果集进行合并的过程。因此,在第二步与第三步之间,还要加入一个合并结果集的方法。
接下来我们看看官方给的操作。官方给出的Collectors里面有toList(),toMap()等多个方法,这里以比较常用的toList方法为例。toList实质上返回了一个CollectorImpl对象,构建这个对象需要以下五个抽象方法:
Supplier<A> supplier()(步骤一使用)
创建一个结果容器A,在toList方法中被重写为new ArrayList()
BiConsumer<A, T> accumulator()(步骤二使用)
主要用于将处理好的元素放入结果集,在toList方法中被重写为list.add(e)
BinaryOperator<A> combiner()(并行流中使用)
将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并,在toList()中被重写为left.addAll(right),其中left和right为两个任务的结果集
Function<A, R> finisher()(步骤三使用)
如果第五个参数中,有IDENTITY_FINISH属性,那么这个方法不会被使用。(toList是有这个属性的)
如果没有,那么会调用此方法将元素逐个转化为集合所希望的类型
Set<Characteristics> characteristics()(标识)
返回一个不可变的Set集合,用来表明该Collector的特征。可能会包含有以下三个特征:
CONCURRENT:表示此收集器支持并发。
UNORDERED:表示该收集操作不会保留流中元素原有的顺序。
IDENTITY_FINISH:表示finisher参数。
下一篇 java8特性 stream笔记(源码解析部分)
https://blog.csdn.net/Ding_Creator/article/details/117665882
参考资料
- https://blog.csdn.net/TheLudlows/article/details/84778817
- https://blog.csdn.net/y4x5M0nivSrJaY3X92c/article/details/83155483?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-17.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-17.control
- https://www.cnblogs.com/CarpenterLee/archive/2017/03/28/6637118.html
- https://blog.csdn.net/weixin_41044036/article/details/113516439
- https://blog.csdn.net/qq_31865983/article/details/106443244
- https://www.cnblogs.com/CarpenterLee/p/6637118.html
- https://blog.csdn.net/xiliunian/article/details/88364200?spm=1001.2014.3001.5502
- https://blog.csdn.net/weixin_41131531/article/details/100007974