java进阶知识--Stream流、方法引用
说到Stream便容易想到I/OStream,而实际上,Stream流不同于IO流。在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念(Stream是Lambda的衍生物),用于解决已有集合类库既有的弊端。
一、Stream流
1.1 概述
我们都知道,遍历是一种目的,循环是一种方式,而要达到遍历的目的,并不是只有循环这一种方式。
循环有它自己的弊端,比如多重循环,代码冗余、效率不高。
基于Lambda的思想(专注于做什么,而不是怎么做。)以及延迟执行,Lambda衍生物Stream流写法的代码中并没有体现使用线性循环或者其他任何算法进行遍历,而是体现出真正需要做的事情,使得代码更加简洁、优雅。
Stream(流)是一个来自数据源的元素队列。
-
- 数据源:流的来源,可以是数组、集合等;
- 元素队列:特定类型的对象,形成一个队列。
和以前的Collection操作不同,Stream操作还有两个基础的特征:
-
- Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,进而对操作进行优化,比如延迟执行(laziness)和短路(short-circuiting)。
- 内部迭代:以前对集合的遍历都是通过Itertator或者增强型for的方式,显示在集合外部进行迭代,这叫做外部迭代。Stream通过内部迭代的方式,流可以字节调用遍历方法。
小贴士:
1. "Stream流"其实是一个集合元素的函数模型。它并不是集合,也不是数据结构,其本身并不存储任何元素或其地址值;
2. 使用Stream流通常包括的三个基本步骤:
获取一个数据源 --> 数据转换 --> 执行操作获取想要的结果。
每次转换原有Stream对象不改变(可以多次转换),返回一个新的Stream对象,这就允许对其操作可以像链条一样排列,变成一个管道。
1.2 获取流
java.util.stream.Stream<T>是java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用方式:
1. Collection获取流
-
-
- 所有的Collection集合都可以通过stream()默认方法获取流;
-
如:list.stream();
2. Map获取流
-
-
- 因为Map是key-value数据结构,不符合元素的单一特性(stream认为key-value是不具有类型的),所以获取对应的流需要分key、value或entry等情况。
-
如:map.keySet().stream();
map.values().stream();
map.entrySet().stream();
3. 数组获取流
-
-
- Stream接口的静态方法 of 可以获取数组对应的流。 如:Stream.of(T... values);
- 获取当前数组的stream流。 如:Arrays.stream(T[] array);
-
由于数组对象不可能添加默认方法,所以Stream接口提供了静态方法 of ,of方法的参数其实是一个可变参数,所以支持数组。
1.3 常用方法
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
-
- 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因为支持链式调用。(除了终结方法外,其余都是延迟方法。)
- 终结方法:返回值类型不再是Stream接口自身类型的方法,因为不再支持链式调用。比如count、forEach方法等。
1.3.1 逐一处理:forEach
-
- void forEach(Consumer<? super T> action);
该方法接收一个Consumer函数式接口函数,会将每一个流元素交给该函数进行处理。
关于Consumer接口函数 https://www.cnblogs.com/sun9/p/13515631.html
1.3.2 过滤:filter
-
- Stream<T> filter(Predicate<? super T> predicate);
该方法接收一个Predicate函数式接口参数,会将一个流转换成另一个子集流。
关于Predicate接口函数 https://www.cnblogs.com/sun9/p/13515631.html
Predicate接口中唯一抽象方法为:boolean test(T t)
- 该方法将会产生一个boolean值结果,代表指定的条件是否满足。
- 如果结果为true,那么Stream流的filter方法将会留用元素;如果结果为false,则会舍弃元素。
1.3.3 映射:map(加工方法)
-
- <R> Stream<R> map(Function<? super T, ? extends R> mapper);
该方法接收一个Function函数式接口参数,可以将当前流中的T类型数据转换成另一种R类型的流(将流中的元素映射到另一个流中)。
关于Function接口函数 https://www.cnblogs.com/sun9/p/13515631.html
Function接口中唯一抽象方法为:R apply(T t); //这是将一种T类型数据转换成另一种R类型数据,而这种转换过程,就称为"映射"。
1.3.4 统计个数:count
-
- long count();
如同Collection集合中的size方法一样,Stream流提供count方法来统计元素个数。
注意:size方法返回的是int值,count方法返回的是long值。
1.3.5 取用前几个:limit
-
- Stream<T> limit( long maxSize); //对流进行截取,只取用前n个元素。
注意:参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。
1.3.6 跳过前几个:skip
-
- Stream<T> skip( long n); //如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流。
注意:如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
1.3.7 组合:concat
-
- static <T> Stream<T> concat(Stream<? extends T> a,Stream<? extends T> b); //将两个流合并成一个流
注意:这是一个静态方法,与java.lang.String 当中的concat方法是不同的。
二、stream流的收集
含义:就是把stream流操作后的结果数据转回到集合或者数组中去。
注:直接用流对象.toLIst()等方法是java 16开始新增的。
三、方法引用
在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?
3.1 方法引用符
双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。
3.1.1 语义分析
例如下面两种写法:
-
- Lambda表达式写法:s -> system.out.println(s);
- 方法引用写法:System.out: : println;
第一种语义是指:拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理。
第二种等效写法的语义是指:直接让system.out 中的println方法来取代Lambda。
两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。
...
...