这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
阅读《Java实战》一书,本文为第3章总结,主要系统梳理Lambda表达式的来龙去脉,以及如何灵活使用。
1、Lambda语法
1.1 特性
作为简洁的可传递匿名函数,没有名词,但又参数列表、函数主体、返回类型,主要有以下几个特点:
- 匿名:没有声明名称。
- 函数:一种函数,不像方法归属于某个类,但和方法一样,有参数列表、函数主体、返回类型。
- 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
- 简洁:相比匿名内部类,不需要很多结构性模板代码。
1.2 语法结构
Lambda表达式主要有三部分构成,参数列表、箭头以及Lambda主体。
- 参数列表:可以是多个也可以是无参。
- 箭头:用来分隔参数列表和Lambda主体。
- Lambda主体:一般分为表达式风格(单行代码)和块风格(多行)。
常见示例:
1 | java复制代码//参数一个String,返回int |
2、Lambda如何使用上
在什么场景下,又如何使用Lambda,对应着两个概念,函数式接口+函数描述符。
2.1 函数式接口
- 只定义一个抽象方法得接口。
- Lambda本质是用内联形式为函数式接口的抽象方法提供实现,一个完整表达式==函数式接口的一个具体实现实例。因此,有函数式接口的方法或者参数,就可以用lambda来提供具体实现。
2.2 函数描述符
函数式接口是Lambda使用基础,而如何使用以及怎么使用,就由其内部抽象方法签名(方法参数+返回值)决定,因此函数式接口的抽象方法,也就是函数描述符,也是Lambda表达式的签名。
2.3 @FunctionalInterface注解
函数式接口建议加上该注解,主要用来声明该接口为函数式接口,编译器会检测该接口是否有且只有声明一个抽象方法。
3、Lambda实战
环绕执行模式是生产中常见的场景,比如资源处理流程’打开一个资源>做一些处理>关闭资源’,其中打开和关闭总是很类似,而中间处理有比较多变,很适合Lambda表达式使用,因此我们以此为例,演示如何将一个传统方法扩展成支持多变Lambda场景:
1 | java复制代码 // 读取文件一行数据,演示一个传统的流程,如何改造成支持lambda的流程 |
总体核心要素是方法通过相应函数式接口,实现行为参数化,使用时再按需通过Lambda表达式快速实现相应的行为定义,完整代码地址。
4、Java8自带函数式接口
为了应用不同的Lambda表达式的需求,Java8已经在java.util.function
下定义了常见的一些函数式接口实现,主要可分为三大类:
- 四种常见类型:
Predicate<T>
、Consumer<T>
、Function<T,R>
、Supplier<T>
- 基础类型特化:
IntPredicate
、LongPredicate
等处理特定具体基础数据类型参数的接口,避免数据类型拆箱、装箱损耗 - 两个参数的扩展:
BiPredicate<T,U>
、BiConsumer<T,U>
、BiFunction<T,U,R>
4.1 Predicate
- 抽象方法:
boolean test(T t)
,接受一个泛型参数,并返回boolean。 - 函数描述符:
T -> boolean
- 适用场景:表示一个涉及类型T的布尔表达式
1 | java复制代码// 一个参数的boolean值函数 |
4.2 Consumer
- 抽象方法:
void accept(T t)
接受一个泛型参数,没有返回值。 - 函数描述符:
T -> void
。 - 适用场景:访问泛型T的对象,只对其执行某些操作,却不需要返回。
4.3 Function<T,R>
- 抽象方法:
R apply(T t)
接受一个泛型T的对象,返回一个泛型R的对象。 - 函数描述符:
T -> R
。 - 适用场景:输入对象映射输出到新对象。
5、Lambda底层机制和规则
前面介绍了怎么用、如何用,接下来进一步深入理解编译器如何处理,以及一些现有的规则。
Lambda的表达式等价于一个函数式接口的实例,但本身却没有包含具体接口信息,因此需要上下文中推测出所需要的类型,也称为目标类型,推断方式主要有三种:
- 方法调用的上下文(参数和返回值)
- 赋值的上下文
- 类型转换的上下文
5.1 类型检查
类型检查主要工作内容:
- 通过调用方法上下文匹配,找出目标类型。
- 通过目标类型的抽象方法,推测出对应函数描述符。
- 解析Lambda表达式签名,与目标类型的函数描述符做匹配检查。
详细流程示例如下图:
5.2 同样Lambda不同的函数式接口
有了目标类型概念,同一个表达式就可以通过赋值的方式和不同的函数式接口联系起来,只要它们表达式兼容。如下:
1 | java复制代码//示例1: |
5.3 参数类型推断
编译器会从上下文(目标类型)推断出匹配Lambda表达式的函数式接口,也就意味着能推断出Lambda的签名,因此函数描述符可以通过目标类型得到,因此可以进一步省略类型,示例如下:
1 | java复制代码//原始格式 |
5.4 局部变量使用限制
Lambda表达式本质和匿名函数类似,因此使用外部变量时,必须是final类型或者事实上final(lambda使用后,后面不可再变更),否则会编译报错,提示如下:
该限制的本质原因:
- 实例变量存储堆中,局部变量保存在栈上。lambda使用的是其副本,而非原始,变更不可见,会导致误解,因此直接禁止变更(Lambda对值封闭,而不是对变量封闭)。
- 不鼓励使用改变外部变量的典型命令式编程模式。
6、方法引用
6.1 定义
重复使用方法的定义,并像Lambda一样传递它们。
- 基础思想:一个Lambda只是描述’直接调用这个方法’,最好名称来调用,而不是去描述如何调用,因为前者可读性更好。
- 基础格式:目标引用放分隔符
::
前,方法名称放后面。
6.2 构建场景
方法引用主要有三类:
- 指向静态方法的方法引用:如Integer的parseInt方法,写作
Integer::parseInt
- 任意类型的实例方法引用:如String的length方法,写作
String::length
- 指向现有对象的实例方法:如局部变量exp的getValue方法,可写作
exp::getValue
此外,还有有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用。
1 | java复制代码//示例1: |
6.2 构造函数引用
可以按构造函数的类型,选择相应的抽象方法来实现。
- 无参构造器,与
Supplier<T>
签名()-> T
一致。
1 | java复制代码//方法引用实现 |
- 一个参数,与
Function<T,R>
函数描述符T -> R
一致。
1 | java复制代码//方法引用实现 |
- 两个参数,与
BiFunction<T,U,R>
签名一致
1 | java复制代码//方法引用实现 |
7、复合Lambda表达式的常见方法
主要是用于将多个简单得Lambda复合成复杂的表达式,主要包含比较器复合、Predicate复合、函数复合。
7.1 比较器的复合
- 逆序
1 | java复制代码inventory.sort(comparing(Apple::getWeight).reversed()); |
- 比较器链:主要用于解决多级比较,比如苹果按重量递减排序,相同重量,再按国家排序
1 | java复制代码inventory.sort(comparing(Apple::getWeight) |
7.2 Predicate复合
主要有negate
-非、and
-与、or
-或三种复合操作。
1 | java复制代码//红色的苹果 |
7.3 Function复合
Function接口配置了andThen
和compose
两个默认方法,用来将其复合。
- andThen:先计算自身,再计算参数的函数
- compose:先计算参数函数,结果作为参数,再计算自身
1 | java复制代码 Function<Integer,Integer> f = (x) -> x+1; |
本文转载自: 掘金