本文为掘金社区首发签约文章,未获受权禁止转载。html
最近在公司写业务的时候,突然想不起来Stream
中的累加应该怎么写?java
无奈只能面向谷歌编程,花费了我宝贵的三分钟以后,学会了,很简单。web
自从我用上JDK8之后,Stream就是我最经常使用的特性,各类流式操做用的飞起,然而此次事之后我突然以为Stream对我真的很陌生。面试
可能你们都同样,对最经常使用到的东西,也最容易将其忽略
,哪怕你要准备面试估计也确定想不起来要看一下Stream这种东西。编程
不过我既然注意到了,就要从新梳理一遍它,也算是对个人总体知识体系的查漏补缺。数组
花了不少功夫来写这篇Stream,但愿你们和我一块从新认识并学习一下Stream,了解API也好,了解内部特性也罢,怕什么真理无穷,进一步有进一步的欢喜。markdown
在本文中我将Stream的内容分为如下几个部分:框架
初看这个导图你们可能对转换流操做和终结流操做这两个名词有点蒙,其实这是我将Stream中的全部API分红两类,每一类起了一个对应的名字(参考自Java8相关书籍,见文末):编程语言
转换流操做 :例如filter和map方法,将一个Stream转换成另外一个Stream,返回值都是Stream。svg
终结流操做 :例如count和collect方法,将一个Stream汇总为咱们须要的结果,返回值都不是Stream。
其中转换流操做的API我也分了两类,文中会有详细例子说明,这里先看一下定义,有一个大概印象:
无状态 :即此方法的执行无需依赖前面方法执行的结果集。
有状态 :即此方法的执行须要依赖前面方法执行的结果集。
因为Stream内容过多,因此我将Stream拆成了上下两篇,本篇是第一篇,内容翔实,用例简单且丰富。
第二篇的主题虽然只有一个终结操做,可是终结操做API比较复杂,因此内容也翔实,用例也简单且丰富,从篇幅上来看二者差很少,敬请期待。
注 :因为我本机的电脑是JDK11,并且写的时候忘了切换到JDK8,因此在用例中大量出现的List.of()
在JDK8是没有的,它等同于JDK8中的Arrays.asList()
。
注 :写做过程当中翻读了大量Stream源码和Java8书籍(文末),创做不易,点赞过百,立刻出第二篇。
一切还要源于JDK8的发布,在那个函数式编程语言如火如荼的时代,Java因为它的臃肿而饱受诟病(强面向对象),社区迫切须要Java能加入函数式语言特色改善这种状况,终于在2014年Java发布了JDK8。
在JDK8中,我认为最大的新特性就是加入了函数式接口和lambda表达式,这两个特性取自函数式编程。
这两个特色的加入使Java变得更加简单与优雅,用函数式对抗函数式,巩固Java老大哥的地位,简直是师夷长技以制夷。
而Stream,就是JDK8又依托于上面的两个特性为集合类库作的 一个类库,它能让咱们经过lambda表达式更简明扼要的以流水线的方式去处理集合内的数据,能够很轻松的完成诸如:过滤、分组、收集、归约这类操做,因此我愿将Stream称为函数式接口的最佳实践。
Stream拥有更清晰的代码结构,为了更好的讲解Stream怎么就让代码变清晰了,这里假设咱们有一个很是简单的需求:在一个集合中找到全部大于2的元素 。
先来看看没使用Stream以前:
List<Integer> list = List.of(1, 2, 3);
List<Integer> filterList = new ArrayList<>();
for (Integer i : list) {
if (i > 2) {
filterList.add(i);
}
}
System.out.println(filterList);
复制代码
上面的代码很好理解,我就不过多解释了,其实也还好了,由于咱们的需求比较简单,若是需求再多点呢?
每多一个要求,那么if里面就又要加一个条件了,而咱们开发中每每对象上都有不少字段,那么条件可能有四五个,最后可能会变成这样:
List<Integer> list = List.of(1, 2, 3);
List<Integer> filterList = new ArrayList<>();
for (Integer i : list) {
if (i > 2 && i < 10 && (i % 2 == 0)) {
filterList.add(i);
}
}
System.out.println(filterList);
复制代码
if里面塞了不少条件,看起来就变得乱糟糟了,其实这也还好,最要命的是项目中每每有不少相似的需求,它们之间的区别只是某个条件不同,那么你就须要复制一大坨代码,改吧改吧就上线了,这就致使代码里有大量重复的代码。
若是你Stream,一切都会变得清晰易懂:
List<Integer> list = List.of(1, 2, 3).stream()
.filter(i -> i > 2)
.filter(i -> i < 10)
.filter(i -> i % 2 == 0)
.collect(toList());
复制代码
这段代码你只须要关注咱们最关注的东西:筛选条件就够了,filter这个方法名能让你清楚的知道它是个过滤条件,collect这个方法名也能看出来它是一个收集器,将最终结果收集到一个List里面去。
同时你可能发现了,为何上面的代码中不用写循环?
由于Stream会帮助咱们进行隐式的循环,这被称为:内部迭代
,与之对应的就是咱们常见的外部迭代了。
因此就算你不写循环,它也会进行一遍循环。
Stream在设计之初就被设计为不可变的
,它的不可变有两重含义:
因为每次Stream操做都会生成一个新的Stream,因此Stream是不可变的,就像String。
在Stream中只保存原集合的引用,因此在进行一些会修改元素的操做时,是经过原元素生成一份新的新元素,因此Stream 的任何操做都不会影响到原对象。
第一个含义能够帮助咱们进行链式调用,实际上咱们使用Stream的过程当中每每会使用链式调用,而第二个含义则是函数式编程中的一大特色:不修改状态。
不管对Stream作怎么样的操做,它最终都不会影响到原集合,它的返回值也是在原集合的基础上进行计算得来的。
因此在Stream中咱们没必要关心操做原对象集合带来的种种反作用,用就完了。
关于函数式编程能够查阅阮一峰的函数式编程初探。
Stream只在遇到终结操做
的时候才会执行,好比:
List.of(1, 2, 3).stream()
.filter(i -> i > 2)
.peek(System.out::println);
复制代码
这么一段代码是不会执行的,peek方法能够看做是forEach,这里我用它来打印Stream中的元素。
由于filter方法和peek方法都是转换流方法,因此不会触发执行。
若是咱们在后面加入一个count方法就能正常执行:
List.of(1, 2, 3).stream()
.filter(i -> i > 2)
.peek(System.out::println)
.count();
复制代码
count方法是一个终结操做,用于计算出Stream中有多少个元素,它的返回值是一个long型。
Stream的这种没有终结操做就不会执行的特性被称为延迟执行
。
与此同时,Stream还会对API中的无状态方法进行名为循环合并
的优化,具体例子详见第三节。
为了文章的完整性,我思来想去仍是加上了建立Stream这一节,这一节主要介绍一些建立Stream的经常使用方式,Stream的建立通常能够分为两种状况:
使用Steam接口建立
经过集合类库建立
同时还会讲一讲Stream的并行流与链接,都是建立Stream,却具备不一样的特色。
Stream做为一个接口,它在接口中定义了定义了几个静态方法为咱们提供建立Stream的API:
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
复制代码
首先是of方法,它提供了一个泛型可变参数,为咱们建立了带有泛型的Stream流,同时在若是你的参数是基本类型的状况下会使用自动包装对基本类型进行包装:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<Double> doubleStream = Stream.of(1.1d, 2.2d, 3.3d);
Stream<String> stringStream = Stream.of("1", "2", "3");
复制代码
固然,你也能够直接建立一个空的Stream,只须要调用另外一个静态方法——empty(),它的泛型是一个Object:
Stream<Object> empty = Stream.empty();
复制代码
以上都是咱们让咱们易于理解的建立方式,还有一种方式能够建立一个无限制元素数量的Stream——generate():
public static<T> Stream<T> generate(Supplier<? extends T> s) {
Objects.requireNonNull(s);
return StreamSupport.stream(
new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
复制代码
从方法参数上来看,它接受一个函数式接口——Supplier做为参数,这个函数式接口是用来建立对象的接口,你能够将其类比为对象的建立工厂,Stream将今后工厂中建立的对象放入Stream中:
Stream<String> generate = Stream.generate(() -> "Supplier");
Stream<Integer> generateInteger = Stream.generate(() -> 123);
复制代码
我这里是为了方便直接使用Lamdba构造了一个Supplier对象,你也能够直接传入一个Supplier对象,它会经过Supplier接口的get() 方法来构造对象。
相较于上面一种来讲,第二种方式更较为经常使用,咱们经常对集合就行Stream流操做而非手动构建一个Stream:
Stream<Integer> integerStreamList = List.of(1, 2, 3).stream();
Stream<String> stringStreamList = List.of("1", "2", "3").stream();
复制代码
在Java8中,集合的顶层接口Collection
被加入了一个新的接口默认方法——stream()
,经过这个方法咱们能够方便的对全部集合子类进行建立Stream的操做:
Stream<Integer> listStream = List.of(1, 2, 3).stream();
Stream<Integer> setStream = Set.of(1, 2, 3).stream();
复制代码
经过查阅源码,能够发先 stream()
方法本质上仍是经过调用一个Stream工具类来建立Stream:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
复制代码
在以上的示例中全部的Stream都是串行流,在某些场景下,为了最大化压榨多核CPU的性能,咱们可使用并行流,它经过JDK7中引入的fork/join框架来执行并行操做,咱们能够经过以下方式建立并行流:
Stream<Integer> integerParallelStream = Stream.of(1, 2, 3).parallel();
Stream<String> stringParallelStream = Stream.of("1", "2", "3").parallel();
Stream<Integer> integerParallelStreamList = List.of(1, 2, 3).parallelStream();
Stream<String> stringParallelStreamList = List.of("1", "2", "3").parallelStream();
复制代码
是的,在Stream的静态方法中没有直接建立并行流的方法,咱们须要在构造Stream后再调用一次parallel()方法才能建立并行流,由于调用parallel()方法并不会从新建立一个并行流对象,而是在原有的Stream对象上面设置了一个并行参数。
固然,咱们还能够看到,Collection接口中能够直接建立并行流,只须要调用与stream()
对应的parallelStream()
方法,就像我刚才讲到的,他们之间其实只有参数的不一样:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
复制代码
不过通常状况下咱们并不须要用到并行流,在Stream中元素不过千的状况下性能并不会有太大提高,由于将元素分散到不一样的CPU进行计算也是有成本的。
并行的好处是充分利用多核CPU的性能,可是使用中每每要对数据进行分割,而后分散到各个CPU上去处理,若是咱们使用的数据是数组结构则能够很轻易的进行分割,可是若是是链表结构的数据或者Hash结构的数据则分割起来很明显不如数组结构方便。
因此只有当Stream中元素过万甚至更大时,选用并行流才能带给你更明显的性能提高。
最后,当你有一个并行流的时候,你也能够经过sequential()
将其方便的转换成串行流:
Stream.of(1, 2, 3).parallel().sequential();
复制代码
若是你在两处构造了两个Stream,在使用的时候但愿组合在一块儿使用,可使用concat():
Stream<Integer> concat = Stream
.concat(Stream.of(1, 2, 3), Stream.of(4, 5, 6));
复制代码
若是是两种不一样的泛型流进行组合,自动推断会自动的推断出两种类型相同的父类:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Stream<String> stringStream = Stream.of("1", "2", "3");
Stream<? extends Serializable> stream = Stream.concat(integerStream, stringStream);
复制代码
无状态方法:即此方法的执行无需依赖前面方法执行的结果集。
在Stream中无状态的API咱们经常使用的大概有如下三个:
map()
方法:此方法的参数是一个Function对象,它可使你对集合中的元素作自定义操做,并保留操做后的元素。
filter()
方法:此方法的参数是一个Predicate对象,Predicate的执行结果是一个Boolean类型,因此此方法只保留返回值为true的元素,正如其名咱们可使用此方法作一些筛选操做。
flatMap()
方法:此方法和map()方法同样参数是一个Function对象,可是此Function的返回值要求是一个Stream,该方法能够将多个Stream中的元素聚合在一块儿进行返回。
先来看看一个map()方法的示例:
Stream<Integer> integerStreamList = List.of(1, 2, 3).stream();
Stream<Integer> mapStream = integerStreamList.map(i -> i * 10);
复制代码
咱们拥有一个List,想要对其中的每一个元素进行乘10 的操做,就能够采用如上写法,其中的i
是对List中元素的变量名,→
后面的逻辑则是要对此元素进行的操做,以一种很是简洁明了的方式传入一段代码逻辑执行,这段代码最后会返回一个包含操做结果的新Stream。
这里为了更好的帮助你们理解,我画了一个简图:
接下来是filter()方法示例:
Stream<Integer> integerStreamList = List.of(1, 2, 3).stream();
Stream<Integer> filterStream = integerStreamList.filter(i -> i >= 20);
复制代码
在这段代码中会执行i >= 20
这段逻辑,而后将返回值为true的结果保存在一个新的Stream中并返回。
这里我也有一个简单的图示:
flatMap()
方法的描述在上文我已经描述过,可是有点过于抽象,我在学习此方法中也是搜索了不少示例才有了较好的理解。
根据官方文档的说法,此方法是为了进行一对多元素的平展操做:
List<Order> orders = List.of(new Order(), new Order());
Stream<Item> itemStream = orders.stream()
.flatMap(order -> order.getItemList().stream());
复制代码
这里我经过一个订单示例来讲明此方法,咱们的每一个订单中都包含了一个商品List,若是我想要将两个订单中全部商品List组成一个新的商品List,就须要用到flatMap()方法。
在上面的代码示例中能够看到每一个订单都返回了一个商品List的Stream,咱们在本例中只有两个订单,因此也就是最终会返回两个商品List的Stream,flatMap()方法的做用就是将这两个Stream中元素提取出来而后放到一个新的Stream中。
老规矩,放一个简单的图示来讲明:
图例中我使用青色表明Stream,在最终的输出中能够看到flatMap()将两个流变成了一个流进行输出,这在某些场景中很是有用,好比我上面的订单例子。
还有一个很不经常使用的无状态方法peek()
:
Stream<T> peek(Consumer<? super T> action);
复制代码
peek方法接受一个Consumer对象作参数,这是一个无返回值的参数,咱们能够经过peek方法作些打印元素之类的操做:
Stream<Integer> peekStream = integerStreamList.peek(i -> System.out.println(i));
复制代码
然而若是你不太熟悉的话,不建议使用,某些状况下它并不会生效,好比:
List.of(1, 2, 3).stream()
.map(i -> i * 10)
.peek(System.out::println)
.count();
复制代码
API文档上面也注明了此方法是用于Debug,经过个人经验,只有当Stream最终须要从新生产元素时,peek才会执行。
上面的例子中,count只须要返回元素个数,因此peek没有执行,若是换成collect方法就会执行。
或者若是Stream中存在过滤方法如filter方法和match相关方法,它也会执行。
上一节提到了三个Stream中最经常使用的三个无状态方法,在Stream的无状态方法中还有几个和map()与flatMap()对应的方法,它们分别是:
mapToInt
mapToLong
mapToDouble
flatMapToInt
flatMapToLong
flatMapToDouble
这六个方法首先从方法名中就能够看出来,它们只是在map()或者flatMap()的基础上对返回值进行转换操做,按理说不必单拎出来作成一个方法,实际上它们的关键在于返回值:
mapToInt返回值为IntStream
mapToLong返回值为LongStream
mapToDouble返回值为DoubleStream
flatMapToInt返回值为IntStream
flatMapToLong返回值为LongStream
flatMapToDouble返回值为DoubleStream
在JDK5中为了使Java更加的面向对象,引入了包装类的概念,八大基础数据类型都对应着一个包装类,这使你在使用基础类型时能够无感的进行自动拆箱/装箱,也就是自动使用包装类的转换方法。
好比,在最前文的示例中,我用了这样一个例子:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
复制代码
我在建立Stream中使用了基本数据类型参数,其泛型则被自动包装成了Integer,可是咱们有时可能忽略自动拆装箱也是有代价的,若是咱们想在使用Stream中忽略这个代价则可使用Stream中转为基础数据类型设计的Stream:
IntStream:对应 基础数据类型中的int、short、char、boolean
LongStream:对应基础数据类型中的long
DoubleStream:对应基础数据类型中的double和float
在这些接口中均可以和上文的例子同样经过of方法构造Stream,且不会自动拆装箱。
因此上文中提到的那六个方法实际上就是将普通流转换成这种基础类型流,在咱们须要的时候能够拥有更高的效率。
基础类型流在API方面拥有Stream同样的API,因此在使用方面只要明白了Stream,基础类型流也都是同样的。
注 :IntStream、LongStream和DoubleStream都是接口,但并不是继承自Stream接口。
说完无状态的这几个方法咱们来看一个前文中的例子:
List<Integer> list = List.of(1, 2, 3).stream()
.filter(i -> i > 2)
.filter(i -> i < 10)
.filter(i -> i % 2 == 0)
.collect(toList());
复制代码
在这个例子中我用了三次filter方法,那么你们以为Stream会循环三次进行过滤吗?
若是换掉其中一个filter为map,你们以为会循环几回?
List<Integer> list = List.of(1, 2, 3).stream()
.map(i -> i * 10)
.filter(i -> i < 10)
.filter(i -> i % 2 == 0)
.collect(toList());
复制代码
从咱们的直觉来看,须要先使用map方法对全部元素作处理,而后再使用filter方法作过滤,因此须要执行三次循环。
但回顾无状态方法的定义,你能够发现其余这三个条件能够放在一个循环里面作,由于filter只依赖map的计算结果,而没必要依赖map执行完后的结果集,因此只要保证先操做map再操做filter,它们就能够在一次循环内完成,这种优化方式被称为循环合并
。
全部的无状态方法均可以放在同一个循环内执行,它们也能够方便的使用并行流在多个CPU上执行。
前面说完了无状态方法,有状态方法就比较简单了,只看名字就能够知道它的做用:
方法名 | 方法结果 |
---|---|
distinct() | 元素去重。 |
sorted() | 元素排序,重载的两个方法,须要的时候能够传入一个排序对象。 |
limit(long maxSize) | 传入一个数字,表明只取前X个元素。 |
skip(long n) | 传入一个数字,表明跳过X个元素,取后面的元素。 |
takeWhile(Predicate predicate) | JDK9新增,传入一个断言参数当第一次断言为false时中止,返回前面断言为true的元素。 |
dropWhile(Predicate predicate) | JDK9新增,传入一个断言参数当第一次断言为false时中止,删除前面断言为true的元素。 |
以上就是全部的有状态方法,它们的方法执行都必须依赖前面方法执行的结果集才能执行,好比排序方法就须要依赖前面方法的结果集才能进行排序。
同时limit方法和takeWhile是两个短路操做方法,这意味效率更高,由于可能内部循环尚未走完时就已经选出了咱们想要的元素。
因此有状态的方法不像无状态方法那样能够在一个循环内执行,每一个有状态方法都要经历一个单独的内部循环,因此编写代码时的顺序会影响到程序的执行结果以及性能,但愿各位读者在开发过程当中注意。
本文主要是对Stream作了一个概览,并讲述了Stream的两大特色:
不可变
:不影响原集合,每次调用都返回一个新的Stream。
延迟执行
:在遇到终结操做以前,Stream不会执行。
同时也将Stream的API分红了转换操做和终结操做两类,并讲解了全部经常使用的转换操做,下一章的主要内容将是终结操做。
在看Stream源码的过程当中发现了一个有趣的事情,在ReferencePipeline
类中(Stream的实现类),它的方法顺序从上往下正好是:无状态方法 → 有状态方法 → 聚合方法。
好了,学完本篇后,我想你们对Stream的总体已经很清晰了,同时对转换操做的API应该也已经掌握了,毕竟也很少😂,Java8还有不少强大的特性,咱们下次接着聊~
同时,本文在写做过程当中也参考了如下书籍:
这三本书都很是好,第一本是Java核心技术的做者写的,若是你想全面的了解JDK8的升级能够看这本。
第二本能够说是一个小册子,只有一百多页很短,主要讲了一些函数式的思想。
若是你只能看一本,那么我这里推荐第三本,豆瓣评分高达9.2,内容和质量都当属上乘。
最后,创做不易,若是对你们有所帮助,但愿你们点赞支持,有什么问题也能够在评论区里讨论😄~