文章目录
何为Stream
- Stream是流的概念, 我们只知道IO流是程序与计算机交互的方式, 而Stream就是得益于Lambda所带来的函数式编程的思想, 将集合中的数据看成一个流, 方便我们对其进行遍历筛选操作
- 比如传统的List或者Map的遍历筛选操作如下
/**
* 传统List遍历筛选操作
*/
public static void everyList() {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
//得到名字长度为3个汉字的集合
List<String> list1 = new ArrayList<>();
for (String name: list
) {
if(name.length() >= 3) {
list1.add(name);
}
}
//得到姓为张的集合
List<String> list2 = new ArrayList<>();
for (String name: list1
) {
if(name.startsWith("张")) {
list2.add(name);
}
}
System.out.println(list2);
}
- 通过上面代码我们就可以发现虽然可读性很好, 但是本来很简单的操作现在变得复杂化, 代码非常冗余
- 传统的遍历筛选操作着重于怎么做和做什么俩点, 但是你看Stream的写法, 他就是主要注重于做什么
/**
* 新特性的遍历筛选操作
*/
public static void streamList() {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.stream().filter(name -> name.length() >= 3)
.filter(name -> name.startsWith("张")).forEach(System.out :: println);
}
- 直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤长度为3、过滤姓张、逐一打印。
- 代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
好处
- Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
- 这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的条件节点上进行处理, 比如筛选, 排序,聚合等。
- 元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。整体来看,流式思想类似于工厂车间的“生产流水线”。
- 这里的 filter 、 map 、 skip 都是在对函数模型进行操作是延迟方法,集合元素并没有真正被处理。只有当终结方法 count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
- 也就是说Stream你不管对数据进行几次筛选, 他只会在遍历一遍源数据流从而得到我们想要的数据结果.
深入Stream
- 一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。其中最主要的中间操作有:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
- 一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。常用的最终操作有:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
对比Collection
和以前的Collection操作不同, Stream操作还有两个基础的特征:
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道。 这样做可以对操作进行优化, 比如延迟执行.
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
- 当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
获取数据源
- 所有的 Collection 集合都可以通过 stream 默认方法获取流;
- Stream 接口的静态方法 of 可以获取数组对应的流。
Collection获取流
首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流.
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream();
Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
Vector<String> vector = new Vector<>();
Stream<String> stream3 = vector.stream();
}
Map获取流
java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:
public static void main(String[] args){
Map<String,String> map = new HashMap<>();
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String,String>> entrys = map.entrySet().stream();
}
数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法of ,还可以使用使用工具类Arrays, 很简单:
public static void main(String[] args){
String[] array = {"私","忆","一","秒","钟"};
Stream<String> stream = Stream.of(array);
Arrays.stream(array).forEach(System.out :: println);
}
PS: of 方法的参数其实是一个可变参数,支持的集合种类很多, 自然数组也就支持
常用方法的使用(重点)
流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
- 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
- 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。终结方法包括 count 和 forEach 方法。
延时方法
过滤:filter
可以通过 filter 方法将一个流转换成另一个子集流。
Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
Predicate接口:
java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为:
boolean test(T t);
- 该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。
public static void main(String[] args){
Stream<String> original = Stream.of("张无忌","张三丰","周芷若");
Stream<String> result = original.filter(s->s.startsWith("张"));
}
映射:map
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Function接口:
java.util.stream.Function 函数式接口,其中唯一的抽象方法为:
R apply(T t);
这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。
public static void main(String[] args){
Stream<String> original = Stream.of("1","2","3");
Stream<Integer> result = original.map(s->Integer.parseInt(s));
}
取前几个:limit
limit 方法可以对流进行截取,只取用前n个。
Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。
public static void main(String[] args){
Stream<String> stream = Stream.of("私","忆","一","秒","钟");
Stream<String> stream2 = stream.limit(2);
System.out.println(stream2.count());
}
跳过前几个:skip
如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
public static void main(String[] args){
Stream<String> stream = Stream.of("私","忆","一","秒","钟");
Stream<String> stream2 = stream.skip(2);
System.out.println(stream2.count());
}
终结方法
统计个数:count
正如旧集合 Collection 当中的 size 方法一样,流提供 count 方法来数一数其中的元素个数:
long count();
该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。
public static void main(String[] args){
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s->s.startsWith("张"));
System.out.println(result.count());
}
消费元素forEach
虽然方法名字叫 forEach ,但是与for循环中的“for-each”昵称不同。
void forEach(Consumer<? super T> action);
该方法接收一个 Consumer 接口函数,会将流中的每一个元素交给该函数进行处理。
Consumer接口:
java.util.function.Consumer接口是一个消费型接口。接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。
public static void main(String[] args){
Stream<String> stream = Stream.of("私","忆","一","秒","钟");
stream.forEach(s->System.out.println(s));
}
- 总结Stream
- 不是数据结构
- 它没有内部存储,它只是用操作管道从 source(数据结构、数组、generator function、IO channel)抓取数据。
- 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
- 所有 Stream 的操作必须以 lambda 表达式为参数
- 不支持索引访问
- 你可以请求第一个元素,但无法请求第二个,第三个,或最后一个。
- 很容易生成数组或者 List
- 惰性化, 延时方法不是立即执行
- 很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始。
- Intermediate 操作永远是惰性化的。
- 并行能力
- 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
- 集合有固定大小,Stream 则不必。limit(n) 和 findFirst() 这类的 short-circuiting 操作可以对无限的 Stream 进行运算并很快完成。