Streams标准函数式接口
前段时间看了InfoQ发表的2019中国Java发展趋势报告,Lambda/Stream语法已经处于晚期大众阶段,在实际开发中确实也给到了很多便利,但对于Lambda/Stream的函数式接口,没有一个系统的学习思路,今天在这边记录一下,巩固一下自己的记忆,也算给不太熟悉Lambda/Stream函数的大家一个学习的方向。
为什么要用Stream
Stream
作为Java8中引入的概念,和java.io
的InputStream
,OutputStream
是不同的概念。可以将Stream
理解为对集合(Collection
)对象功能的增强,提供了大量的聚合操作,Lambda简明易懂的语法,相对于原有的Step by step操作,在代码的可读性提升的同时,大大降低了代码的复杂度。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选、排序、聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由终端操作 (terminal operation) 得到前面处理的结果。
在学习使用Stream
API前,首先要熟悉一下Java8中引入的几个新的函数式接口,Function
、Predicate
、Consumer
和Supplier
。
什么是函数式接口
函数式接口是在Java8中引入的概念,即可用于配合Lambda进行使用的接口。既然名叫函数式接口,那么首先它得是一个接口,其次在接口中有且只能有一个抽象方法。我们都知道在Java中,接口内方法的默认修饰符是public abstract
, 即我们也可以说只有一个方法的接口就是函数式接口,虽然有注释@FunctionalInterface
可用于标注函数式接口,但该注释不是必须的,加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface
,那么编译器会报错。
Function
1 | /** |
从接口的Java Doc来看,Function<T, R>
表示接受一个参数的函数,输入类型为 T
,输出类型为 R
。Function
接口只包含一个抽象方法 R apply(T t)
,也就是在类型为 T
的输入 t
上应用该函数,得到类型为 R
的输出。
1 | Function<Integer, String> funcDemo = (x) -> "Input value is " + x; |
除了接受一个参数的 Function
之外,还有接受两个参数的接口 java.util.function.BiFunction<T, U, R>
,T
和 U
分别是两个参数的类型,R
是输出类型。BiFunction
接口的抽象方法为 R apply(T t, U u)
。超过 2 个参数的函数在 Java 标准库中并没有定义。如果函数需要 3 个或更多的参数,可以使用第三方库,如 Vavr
中的 Function0
到 Function8
。
Predicate
1 | /** |
Predicate
接口从字面意思上来看是用于判断验证一部分逻辑接口,输入参数为需进行验证的对象,输出为Boolean
类型,抽象方法为 boolean test(T t)
。
1 | Predicate<String> predicateDemo = (s) -> s.equals("HelloWorld"); |
Supplier
1 | /** |
Supplier<T>
:没有输入,一个输出。抽象方法为 T get()
。
1 | Supplier<StringBuilder> supplierDemo = StringBuilder::new; |
Consumer
1 | /** |
Consumer<T>
:接受一个输入,没有输出。抽象方法为 void accept(T t)
。
1 | Consumer<List<String>> consumerDemo = (list) -> list.add("Here is consumer"); |
Stream API
映射
可能在我们的日常开发过程中经常会遇到将一个集合转换成另外一个对象的集合,那么这种操作放到 Stream 流中就是映射操作。映射操作主要就是将一个 Stream 流转换成另外一个对象的 Stream 流或者将一个 Stream 流中符合条件的元素放到一个新的 Stream 流里面。
这边主要讨论使用Stream中常用的两个两个映射方法,map(Function mapper)
和flatMap(Function mapper)
。
1 | public interface Stream<T> extends BaseStream<T, Stream<T>> { |
map()
我们可以看到map()
方法的参数是一个Function
类型,该方法可以将一个流转换成另外一种对象的流,其中的 T
是原始流中元素的类型,而 R
则是转换之后的流中元素的类型。在以下代码中我们将Integer
类型的集合内元素拼接上前缀,并使用换行符连接输出。
1 | String result = Arrays.asList(1, 2, 3).stream() // 获取集合的流对象 |
flatMap()
flatMap()操作能把原始流中的元素进行一对多的转换,并且将新生成的元素全都合并到它返回的流里面。
1 | List<String> list = Arrays.asList("192.168.0.1", "192.168.0.2", "192.168.0.3").stream() // 获取集合流对象 |
例子不是很好,但是主要是看一个使用方法。
过滤和收集
当我们要在一系列结果中过滤一部分结果,并将其转换为集合、Map、字符串或者是其他类型的话,Stream
API中的filter()
方法和collect()
方法就派上了用场,我们先来看看这两个方法的方法签名。
1 | public interface Stream<T> extends BaseStream<T, Stream<T>> { |
filter()
filter()
方法的参数是一个Predicate
对象,在之前了解几个基础函数式接口的时候接触过。这边我们直接看一下它的用法。
1 | List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).stream() // 获取集合的流对象 |
collect()
收集操作相当于将流管道内的计算结果转换成你想要的目标类型。collect()
重载了两个方法,我们先看一个参数的collect()
方法。
collect(Collector<? super T, A, R> collector)
其中 R
指定结果的类型,T
指定了调用流的元素类型。内部积累的类型由 A
指定。collector
是一个收集器,指定收集过程如何执行,collect()
方法是一个终端方法。一般情况我们只需要借助 Collectors
中的方法就可以完成收集操作。
常用的有Collectors.toList()
、Collectors.joining()
、Collectors.toMap()
等等,之前的实例很多都用到了这个重载方法,此处就不再编写用例了。
collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
该重载方法相当于Collectors
类的具体实现,通过自定义源类型完成收集操作。我们可以尝试通过collect()
方法将一个字符串集合的首字母作为Map的键,整个字符串作为Map的值作为此次用例。
1 | Map<String, String> result = Arrays.asList("Amazon", "ByteDance", "DiDi", "Netease", "Tencent").stream() // 获取字符串集合的Stream流对象 |
以上小结谈论了几个Stream
流常用的API,Stream
的API还有很多,由于篇幅有限就不逐一介绍并编写用例了,在理解了基础的函数式接口的使用之后,其他的方法相信也能融会贯通。
并行流和顺序流
在现在的大流量时代,为有效的应对高并发问题,合理的使用线程资源是很重要的一环。Java8的Stream
流具体提供了并行流和顺序流,通过字面意思就能理解,并行流是通过讲流内的一个大人物分割为很多的小任务然后分配给每条线程执行,线程执行完后讲结果返回之后合并为总结果。而顺序流就很简单了,是通过单线程完成所有的操作。并行流的概念用中国人的概念来说可以理解为分而治之,在Java中能对应到Fork/join框架,就是在必要得情况下,将一个大任务,进行拆分(Fork)成若干个小任务(拆分到不能再拆分),再将一个个的小任务运算得结果进行join汇总。本篇由于主要是对Stream
进行讨论。Fork/Join框架到此便不再细说。
小结
Stream
流在日常的开发中简化了我们大量的逻辑代码,简化了开发过程提高了代码可读性,在日后的开发过程中,也建议多使用Stream
的特性,提高代码书写的流畅程度。
Ps: Stream这玩意…学完之后真的回不去…有点爽