lambda表达式和stream流
lambda表达式
基本格式
(Object o1, Object o2) -> {方法体}
o1、o2:方法参数
->:特定的箭头符号
{...}:方法体内容,实际的代码
注:当方法体只有一行时,花括号可省略。
函数式接口
函数式接口就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
@FunctionalInterface
java提供上述注解标记一个接口为函数式接口,该注解为非强制添加注解,仅仅作为一个定义的条件约束,也就是说如果一个接口加上了该注解,那么该接口必须满足函数式接口的定义。
注:重写/定义父类的方法,不影响上述的判断规则,接口只要符合上述规则即可。
核心函数式接口
消费接口
Consumer<T>,接口源码如下:
顾名思义,该接口的accept方法实现要求传入一个泛型参数,在方法体中做出某些操作,无返回值(void),就像现实生活中的某些消费操作一样,把钱交给对方后,对方提供某些服务。该接口大多数用于集合遍历操作中(可以通过项目代码看出),对元素进行消费操作。
常见变种接口:
生产接口
Supplier<T>,接口源码如下:
该接口get方法实现无入参,返回值为指定泛型类型,该接口可以用来生产元素。
常见变种接口:
谓词接口
Predicate<T>,接口部分源码如下:
该接口test方法实现要求传入一个泛型参数t,返回值为boolean,因此该接口常常用来做元素的过滤和筛选。
常见变种接口:
函数接口
Function<T, R>,接口部分源码如下:
该接口apply方法实现要求传入一个泛型参数t,返回值为另一个泛型类型R,该接口主要用来进行元素的映射和转换。
常见变种接口:
类型检查
Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型。类型检查描述示例如下图所示(部分图例摘自《Java8实战》)。
类型推断
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名(方法名+参数类型),因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。类型推断示例如下图所示。
方法引用
方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。方法引用的一些例子如下图所示。
方法引用构建方法主要有以下三类。
静态方法的调用
当调用类的静态方法时,可以简写为类名::方法名(不带括号)的格式。
额外补充说明如下:
无参静态方法调用
可以直接使用方法引用。
具体示例代码如下:
Test类静态方法:
Test类main方法代码:
带参的静态方法引用
调用的静态方法中的参数个数,必须要和lambda参数个数保持一致,且不能包含其他外部参数,否则无法使用方法引用。
具体示例代码如下:
Test类中带参的静态方法如下:
Test2类中带参的静态方法如下:
Test类main方法代码:
Test类静态方法,3个参数:
Test类main方法代码:
lambda表达式中参数对象的实例方法调用
当调用lambda表达式中参数对象的实例方法时,可以简写为类名::调用方法名(不带括号)的格式。
额外补充说明如下:
无参实例方法调用
lambda参数仅有一个时,可以将其转换为方法引用;
若lambda参数多个,则无法转换为方法引用。
具体示例代码如下:
Test类,无参实例方法如下:
TestInterface接口如下,test方法仅有一个参数:
Test类main方法代码:
TestInterface1接口如下,test方法包含多个参数:
Test类main方法代码:
带参实例方法调用
lambda参数仅有一个时,无论实例方法参数类型和个数,均不能使用方法引用;
lambda参数为多个时,必须满足方法体中调用方法的对象为lambda参数的第一个参数,并且剩余的参数,类型和个数要和调用的方法的类型和个数匹配,且剩余参数均要在调用方法的其他参数列表里,个数只能出现一次,不允许出现其他外部参数。
具体示例代码如下:
Test类中,带参实例方法如下:
Test类main方法代码:
Test类中,带参实例方法如下:
Test类main方法代码:
将TestInterface接口的test方法修改为如下格式,包含3个参数:
Test类main方法代码:
外部实例对象的实例方法调用
当调用其他实例对象的实例方法时,可以简写为实例对象名::调用方法名(不带括号)的格式。
额外补充说明如下:
无参实例方法调用
不存在lambda参数时,可以使用方法引用;
存在lambda参数时,不可以使用方法引用。
具体示例代码如下:
TestInterface3接口如下:
TestInterface4接口如下:
Test类,main方法代码:
带参实例方法调用
不存在lambda参数时,无法使用方法引用;
存在lambda参数,且lambda参数和调用的实例方法参数类型、个数、顺序保持一致,则可以使用方法引用。
具体示例代码如下:
TestInterface接口代码如下:
TestInterface3接口代码如下:
Test类,实例方法如下:
Test类,main方法代码:
Stream流
基本概念
流是Java8新增的特性,简单来说,流能让你更加简单地对数据的集合进行排序、过滤、映射等处理。使用流有以下几点好处:声明性(代码更简洁、易读)、可复合(更灵活)、可并行(充分发挥性能)。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等;元素流在管道中经过中间操作的处理,最后由最终操作得到前面处理的结果。
可以将stream流操作想象成工厂里的流水线操作,原料经过一系列的流水线加工,最终包装成产品。
中间/终端操作
java.util.stream.Stream中的Stream接口定义了许多操作,它们可以分为两大类,以下列代码举例说明:
List<String> names = menu.stream()
.filter(d -> d.getCalories() > 300)(过滤)
.map(Dish::getName)(映射)
.limit(3)(取前3条数据)
.collect(toList());(聚合)
其中,filter、map和limit可以连成一条流水线;collect触发流水线执行并关闭它。
可以连接起来的流操作称为中间操作。诸如 filter 或 sorted 等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理(延迟操作)。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
关闭流的操作称为终端操作。终端操作会从流的流水线生成结果,该结果是任何不是流的值,比如List、Integer甚至是void(如进行遍历操作)。
使用流的步骤
第一步:从一个数据源(如集合)调用指定方法,获取流对象;
第二步:对于流对象,可以有一个或多个中间操作链,形成一条流的流水线操作;
第三步:执行某个终端操作,流水线此时才开始执行,并生成结果。
流的创建
由值创建流:Stream.of()静态方法,传入对象/对象数组;
由数组创建流:Arrays.stream()静态方法,传入数组;
由文件创建流:Files.lines()静态方法,传入文件路径(Path);
由函数创建流:Stream. iterate()静态方法(迭代),Stream.generate()静态方法(生成)。
注:使用函数创建流时,一定要调用limit方法限制流元素的个数,否则流会无限生成元素,导致程序运行异常。
示例代码:
收集器
Collector<T, A, R>
接口部分源码如下
泛型说明:
T:进行归约操作的元素类型;
A:归约操作的中间类型;
R:归约操作最终返回的类型。
方法说明:
supplier():流元素容器提供;
accumulator():流元素归约操作;
combiner():将两个部分结果合并到一个结果中。该方法适用于parallelStream()并行流;
finisher():将中间类型结果转换为最终类型;
characteristics():收集器的特性。