关于java中Stream理解

时间:2021-09-14 19:49:38

关于java中Stream理解

Stream是什么

Stream:Java 8新增的接口,Stream可以认为是一个高级版本的Iterator。它代表着数据流,流中的数据元素的数量可以是有限的,

也可以是无限的。

Stream跟Iterator的差别是

无存储:流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。

函数式编程:对数据流的任何修改都不会修改背后的数据源,比如对流执行滤波器操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新的流。

惰性执行:Stream的操作由零个或多个中间操作(中间操作)和一个结束操作(terminal operation)两部分组成。只有执行了结束操作,Stream定义的中间操作才会依次执行,这就是Stream延迟特性。从后往前追溯,直到起始。

惰性求值(lazy evaluation):惰性求值是一种将对函数或请求的处理延迟到真正需要结果时进行求值的方式。

可消费性:流只能被“消费”一次,一旦遍历过就会失效就像容器的迭代器那样,想要再次遍历必须重新生成一个新的数据流。

对stream的操作分为为两类,二者特点是:

中间操作(intermediate operations) 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新stream,仅此而已。

结束操作(terminal operations) 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

为什么要使用stream

代码简洁,函数式编程写出的代码简洁且意图明确,使用stream接口让你从此告别for循环。

多核友好,Java函数式编程使得编写并行程序从未如此简单,你需要的全部就是调用一下parallel()方法。

源码解析与使用示例

1.如何创建Stream

  • Collection.stream()或者Collection.parallelStream()

  • 调用Arrays.stream(T[] array)方法。

  • 使用Stream.builder()使用.add(x)添加元素使用build()完成构建

  • Stream.of来创建

2.操作方式

以下是部分常用操作方式

操作类型 接口方法
中间操作 caoncat() distinct() limit() peek() skip() sorted() filter()
parallel() sequential() unordered() map() flatMap()
结束操作 forEach() forEachOrdered() toArray() findAny() findFirst()
allMatch() anyMatch() noneMatch() reduce() max() min() count() collect()

通常情况下Stream和函数接口关系非常紧密,没有函数接口steam就无法工作。而函数接口出现的地方都可以使用lambda表达式.

中间操作

在讲解操作之前先构造一些基础的实体类和处理逻辑

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Student { private Integer id; private String name; private Boolean sex; private Double height; } //原始数据
private static List<Student> getWList() {
Student s7 = new Student(7,"貂蝉",false,1.65);
Student s8 = new Student(8,"小乔",false,1.61);
Student s9 = new Student(8,"小乔",false,1.61);
List<Student> wList = new ArrayList<>();
wList.add(s7);
wList.add(s8);
wList.add(s9);
return wList;
} private static List<Student> getMList() {
Student s1 = new Student(1,"刘备",true,1.72);
Student s2 = new Student(2,"关羽",true,1.80);
Student s3 = new Student(3,"张飞",true,1.82);
Student s4 = new Student(4,"黄忠",true,1.81);
Student s5 = new Student(5,"马超",true,1.70);
Student s6 = new Student(6,"赵云",true,1.75);
List<Student> mList = new ArrayList<>();
mList.add(s1);
mList.add(s2);
mList.add(s3);
mList.add(s4);
mList.add(s5);
mList.add(s6);
return mList;
}

每次调用方法都会创建新的list对象,不会出现list耗尽的情况

在学习方法之前有一个新的操作符需要理解

:: 操作符: 表示调用该对象的某方法 Student::getHeight 等同于 x->x.getgetHeight() 等同于 (x) -> {return x.getHeight()} System.out::println

等同于x -> System.out.println(x)

什么是双冒号操作符

1. 连接 caoncat()

创建一个延迟的连接流

接口

public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b); @SuppressWarnings("unchecked")
Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
return stream.onClose(Streams.composedClose(a, b));
}

操作过程

// 合并 concat()
Stream<Student> stream = Stream.concat(mList.stream(), wList.stream());
// 结束操作
streamSorted.forEach(System.out::println);

打印结果:

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=2, name=关羽, sex=true, height=1.8)

Student(id=3, name=张飞, sex=true, height=1.82)

Student(id=4, name=黄忠, sex=true, height=1.81)

Student(id=5, name=马超, sex=true, height=1.7)

Student(id=6, name=赵云, sex=true, height=1.75)

Student(id=7, name=貂蝉, sex=false, height=1.65)

Student(id=8, name=小乔, sex=false, height=1.61)

Student(id=8, name=小乔, sex=false, height=1.61)

打印过结果显示是该流被整合在了一起

2. 去重 distinct()

除去重复元素,如果是对比对象内容需要重写equals方法

接口

Stream<T> distinct();

具体实现

// 去重 distinct()
Stream<Student> streamDistinct = wList.stream().distinct();
streamSorted.forEach(System.out::println);

打印结果:

Student(id=7, name=貂蝉, sex=false, height=1.65)

Student(id=8, name=小乔, sex=false, height=1.61)

可以看出多余的小乔被移除了

3.截断 limit()

截断流,使其元素不超过给定数量,可以理解为for循环执行到一定数量以后break。

接口

Stream<T> limit(long maxSize);

具体实现

// 截断 limit()
Stream<Student> streamLimit = mList.stream().limit(3);
streamSorted.forEach(System.out::println);

打印结果

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=2, name=关羽, sex=true, height=1.8)

Student(id=3, name=张飞, sex=true, height=1.82)

可以看出流满足三个元素以后就被截断了。

4.跳过 skip()

跳过几个元素,返回一个丢掉了前n个元素的流,若元素不足n个则返回一个空的流。与limit()互补。

接口

Stream<T> skip(long n);

具体实现

// 跳过 skip
Stream<Student> streamSkip = mList.stream().skip(3);
streamSkip.forEach(x -> System.out.println(x.toString()));

打印结果

Student(id=4, name=黄忠, sex=true, height=1.81)

Student(id=5, name=马超, sex=true, height=1.7)

Student(id=6, name=赵云, sex=true, height=1.75)

可以看出前三个元素被丢掉了

limit方法时从开始到limit的参数位置个元素留下其他的丢掉。skip方法时从开始到skip的参数个元素丢掉其他的留下。

5.排序 sorted

将元素按照某种既定的顺序排序

接口

// 默认比较器 值可排序的情况下
Stream<T> sorted();
// 比较器排序 自定义比较器
Stream<T> sorted(Comparator<? super T> comparator);

实现方式 应为student是对象无法使用默认比较器。

System.out.println("排序 sorted方法");

System.out.println("排序前 stream");
Stream<Student> streamSorted = getMList().stream();
streamSorted.forEach(System.out::println); // 第一种自定义比较方法
System.out.println("排序后 stream");
streamSorted = getMList().stream().sorted((x,y)->{
if(x.getHeight().equals(y.getHeight())){
return x.getName().compareTo(y.getName());
}else{
return x.getHeight().compareTo(y.getHeight());
}
});
streamSorted.forEach(System.out::println);
System.out.println("排序后 stream");
// 第二种自定义比较方法
streamSorted = getMList().stream().sorted(Comparator.comparingDouble(Student::getHeight));
streamSorted.forEach(System.out::println);

打印结果

排序 sorted方法

排序前 stream

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=2, name=关羽, sex=true, height=1.8)

Student(id=3, name=张飞, sex=true, height=1.82)

Student(id=4, name=黄忠, sex=true, height=1.81)

Student(id=5, name=马超, sex=true, height=1.7)

Student(id=6, name=赵云, sex=true, height=1.75)

排序后 stream

Student(id=5, name=马超, sex=true, height=1.7)

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=6, name=赵云, sex=true, height=1.75)

Student(id=2, name=关羽, sex=true, height=1.8)

Student(id=4, name=黄忠, sex=true, height=1.81)

Student(id=3, name=张飞, sex=true, height=1.82)

6. 转化为并行流 parallel()

将程序并行执行,执行顺序并不是有序的。

接口

//该方法时存在于BaseStream
S parallel();

具体实现

Stream<Student> streamParallel = getMList().stream();
streamParallel.parallel().forEach(System.out::println);

打印结果

Student(id=2, name=关羽, sex=true, height=1.8)

Student(id=3, name=张飞, sex=true, height=1.82)

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=5, name=马超, sex=true, height=1.7)

Student(id=6, name=赵云, sex=true, height=1.75)

Student(id=4, name=黄忠, sex=true, height=1.81)

可以看出以上执行顺序已经乱了。

7. 转化为顺序执行 sequential()

将流转换为顺序执行

接口

 S sequential();

使用方式和 parallel类似,通常情况下流都是顺序执行的。

8. 标记无序化 unordered()

将这个流标记为无序,但是不会打乱其顺序。

接口

S unordered();
9.摊平 flatMap()

将一个一个list摊平为Student流

接口

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

实现方式

log.info("摊平 flatMap");
Stream<List<Student>> streamFlatMap1 = Stream.of(getWList(),getMList());
streamFlatMap1.forEach(System.out::println);
Stream<List<Student>> streamFlatMap = Stream.of(getWList(),getMList());
streamFlatMap.flatMap(Collection::stream).forEach(System.out::println);

打印结果

*[Student(id=7, name=貂蝉, sex=false, height=1.65),

Student(id=8, name=小乔, sex=false, height=1.61),

Student(id=8, name=小乔, sex=false, height=1.61)]

[Student(id=1, name=刘备, sex=true, height=1.72),

Student(id=2, name=关羽, sex=true, height=1.8),

Student(id=3, name=张飞, sex=true, height=1.82),

Student(id=4, name=黄忠, sex=true, height=1.81),

Student(id=5, name=马超, sex=true, height=1.7),

Student(id=6, name=赵云, sex=true, height=1.75)]

Student(id=7, name=貂蝉, sex=false, height=1.65)

Student(id=8, name=小乔, sex=false, height=1.61)

Student(id=8, name=小乔, sex=false, height=1.61)

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=2, name=关羽, sex=true, height=1.8)

Student(id=3, name=张飞, sex=true, height=1.82)

Student(id=4, name=黄忠, sex=true, height=1.81)

Student(id=5, name=马超, sex=true, height=1.7)

Student(id=6, name=赵云, sex=true, height=1.75)*

10. 转换映射 map()

就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

接口

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

具体实现

log.info("转换映射 map");
Stream<Student> streamMap = getMList().stream();
//这里由一个Student流转换为一个double流
streamMap.map(Student::getHeight)
.peek(x-> System.out.println(x.getClass().getName()))
.forEach(System.out::println);

打印结果

09:52:08.665 [main] INFO stream.StreamApiExample - 转换映射 map

java.lang.Double

1.72

java.lang.Double

1.8

java.lang.Double

1.82

java.lang.Double

1.81

java.lang.Double

1.7

java.lang.Double

1.75


可以看出该流已经由Student流转换为一个double流了

结束操作

中间操作讲解完成了,其他的部分后续会补充。其实在讲解中间流的时候已经使用到一种结束操作,这就是遍历操作。

1.遍历操作 forEach() forEachOrdered()

如果流具有已定义的相遇顺序,则按流的相遇顺序对此流的每个元素执行操作。

主要区别在并行处理上,forEach方法不能保证在并行状态下流的执行是按照顺序出现的。而forEachOrdered()可以保证

接口

  void forEachOrdered(Consumer<? super T> action);
void forEach(Consumer<? super T> action);

具体实现

IntStream.range(1,5).parallel().forEachOrdered(System.out::println);
IntStream.range(1,5).parallel().forEach(System.out::println);

打印结果

*1

2

3

4

1

2

4

3*

可以看到顺序被打乱了

注意:只有在并行状态下才会有区别。

2. 转换为数组 toArray()

将流对象转换为数组

Object[] toArray();

实现方式

Stream<Student> streamToArray = getMList().stream();
Object[] arr = streamToArray.toArray();
System.out.println(Arrays.toString(arr));

打印结果

[Student(id=1, name=刘备, sex=true, height=1.72),

Student(id=2, name=关羽, sex=true, height=1.8),

Student(id=3, name=张飞, sex=true, height=1.82),

Student(id=4, name=黄忠, sex=true, height=1.81),

Student(id=5, name=马超, sex=true, height=1.7),

Student(id=6, name=赵云, sex=true, height=1.75)]

3. 发现元素 findFirst() findAny()

在顺序的情况两个方法都是下返回第一个元素

在乱序的情况下 findFirst()返回的永远是顺序状态下的第一个元素。

findAny()返回的是第一个执行完的元素。

接口

Optional<T> findFirst();
Optional<T> findAny();

实现方式

log.info(" findFirst()和findAny");
Stream<Student> streamFindFirst = getMList().stream();
Optional<Student> stu = streamFindFirst.parallel().findFirst();
System.out.println(stu.get());
Stream<Student> streamFindAny = getMList().stream();
Optional<Student> stuAny = streamFindAny.parallel().findAny();
System.out.println(stuAny.get());

打印结果

Student(id=1, name=刘备, sex=true, height=1.72)

Student(id=2, name=关羽, sex=true, height=1.8)

4.条件全匹配 allMatch()noneMatch() anyMatch()

allMatch() 所有元素是否匹配该条件

noneMatch() 没有一个符合匹配条件

anyMatch() 至少有一个符合匹配条件

接口

//方法api
boolean test(T t); boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);

实现方式

log.info("allMatch()");
Stream<Student> streamAllMatch = getMList().stream();
boolean res = streamAllMatch.allMatch(x -> x.getId() < 100);
System.out.println(res);

打印结果

true

注意:如果流是空的任意匹配都返回true 空虚真理

5. 聚合操作 reduce() sum()、max()、min()、count()

reduce() 自定义聚合操作

max() 求最大

min() 求最小

count() 统计数量

接口


// 元素如何合并
Optional<T> reduce(BinaryOperator<T> accumulator);
// 顺序执行时 初始值 identity 元素如何合并操作 accumulator
T reduce(T identity, BinaryOperator<T> accumulator);
// 在并行执行时
<U> U reduce(U identity,//初始值
BiFunction<U, ? super T, U> accumulator, //如何合并成一部分
BinaryOperator<U> combiner); //多部分如何合并
// 求最大 传入比较器
Optional<T> max(Comparator<? super T> comparator);
// 求最小 传入比较器
Optional<T> min(Comparator<? super T> comparator);
//统计元素个数
long count();

使用示例1

log.info("reduce() 一般都和map配合使用");
log.info("统计身高平均值");
Stream<Student> streamReduce = getMList().stream();
long count = streamReduce.count();
Optional<Double> total = getMList().stream().map(Student::getHeight)
.reduce((a, b) -> a + b);
System.out.println(total.get()/count);

结果

1.7666666666666666

5.collect()

该方法可以解决大多数啊上面api解决不了的问题。

接口

<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner); <R, A> R collect(Collector<? super T, A, R> collector);

使用示例:

将student对象中的name作为key height作为value生成一个Map

Stream<Student> streamollect = getMList().stream();
Map<String,String> s = streamReduce.collect(HashMap::new,
(map,stu)-> map.put(stu.getName(),stu.getHeight()+""),
HashMap::putAll);
System.out.println(s.toString());

结果:

{关羽=1.8, 张飞=1.82, 刘备=1.72, 马超=1.7, 赵云=1.75, 黄忠=1.81}

将流对象中的名称取出形成一个列表

Stream<Student> streamCollect = getMList().stream();
ArrayList<Object> list = streamCollect.collect(
ArrayList::new,
(arr, stu) -> arr.add(stu.getName()),
ArrayList::addAll);
System.out.println(list.toString());

结果:

[刘备, 关羽, 张飞, 黄忠, 马超, 赵云]

执行过程

总结

collect的用法还有很多很多。从根本上将Stream是解决for循环不能并行优化的问题.而在这其中很多的lambda的方法都很违反常规的java写法。不过随着lambda的加入将会有越来越多的函数式编程方式来实现java的功能。

引用

https://github.com/kanwangzjm/JavaLambdaInternals