系统学Java新特性-Lambda表达式

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

阅读《Java实战》一书,本文为第3章总结,主要系统梳理Lambda表达式的来龙去脉,以及如何灵活使用。

1、Lambda语法

1.1 特性

作为简洁的可传递匿名函数,没有名词,但又参数列表、函数主体、返回类型,主要有以下几个特点:

  • 匿名:没有声明名称。
  • 函数:一种函数,不像方法归属于某个类,但和方法一样,有参数列表、函数主体、返回类型。
  • 传递:Lambda表达式可以作为参数传递给方法或者存储在变量中。
  • 简洁:相比匿名内部类,不需要很多结构性模板代码。

1.2 语法结构

Lambda表达式主要有三部分构成,参数列表、箭头以及Lambda主体。

  • 参数列表:可以是多个也可以是无参。
  • 箭头:用来分隔参数列表和Lambda主体。
  • Lambda主体:一般分为表达式风格(单行代码)和块风格(多行)。

常见示例:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码//参数一个String,返回int
(String s)-> s.length();
//一个对象类型参数,返回boolean
(Apple a)-> a.getWeight()>150;
// 无参,返回一个对象
()-> new Apple(10);
// 无返回值,消费对象,块风格主体
(Apple a)-> {
System.out.println(a.getWeigth());
}
// 两个参数,返回int,对象比较
(Apple a1,Apple a2)-> a1.getWeight().compareTo(a2.getWeight());

2、Lambda如何使用上

在什么场景下,又如何使用Lambda,对应着两个概念,函数式接口+函数描述符。

2.1 函数式接口

  • 只定义一个抽象方法得接口。
  • Lambda本质是用内联形式为函数式接口的抽象方法提供实现,一个完整表达式==函数式接口的一个具体实现实例。因此,有函数式接口的方法或者参数,就可以用lambda来提供具体实现。

2.2 函数描述符

函数式接口是Lambda使用基础,而如何使用以及怎么使用,就由其内部抽象方法签名(方法参数+返回值)决定,因此函数式接口的抽象方法,也就是函数描述符,也是Lambda表达式的签名。

2.3 @FunctionalInterface注解

函数式接口建议加上该注解,主要用来声明该接口为函数式接口,编译器会检测该接口是否有且只有声明一个抽象方法。

3、Lambda实战

环绕执行模式是生产中常见的场景,比如资源处理流程’打开一个资源>做一些处理>关闭资源’,其中打开和关闭总是很类似,而中间处理有比较多变,很适合Lambda表达式使用,因此我们以此为例,演示如何将一个传统方法扩展成支持多变Lambda场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码    // 读取文件一行数据,演示一个传统的流程,如何改造成支持lambda的流程
// 传统实现:固定写死逻辑
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))){
return br.readLine();
}
}
//基于Lambda扩展
//1、行为参数化:
//1.1 引入函数式接口
@FunctionalInterface
interface ProcessFilePredicate{
String process(BufferedReader br) throws IOException ;
}
//1.2 用函数式接口传统行为
public static String processFile(ProcessFilePredicate p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))){
return p.process(br);
}
}

public static void main(String[] args) throws IOException {
System.out.println(processFile());
System.out.println("---");
// 基于Lambda的灵活使用
String oneLine = processFile((BufferedReader br) -> br.readLine());
System.out.println(oneLine);
System.out.println("---");
String twoLine = processFile((BufferedReader br) -> br.readLine()+br.readLine());
System.out.println(twoLine);
}

总体核心要素是方法通过相应函数式接口,实现行为参数化,使用时再按需通过Lambda表达式快速实现相应的行为定义,完整代码地址

4、Java8自带函数式接口

为了应用不同的Lambda表达式的需求,Java8已经在java.util.function下定义了常见的一些函数式接口实现,主要可分为三大类:

  • 四种常见类型:Predicate<T>Consumer<T>Function<T,R>Supplier<T>
  • 基础类型特化:IntPredicateLongPredicate等处理特定具体基础数据类型参数的接口,避免数据类型拆箱、装箱损耗
  • 两个参数的扩展:BiPredicate<T,U>BiConsumer<T,U>BiFunction<T,U,R>

4.1 Predicate

  • 抽象方法: boolean test(T t),接受一个泛型参数,并返回boolean。
  • 函数描述符: T -> boolean
  • 适用场景:表示一个涉及类型T的布尔表达式
1
2
3
4
5
java复制代码// 一个参数的boolean值函数
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
}

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表达式签名,与目标类型的函数描述符做匹配检查。

详细流程示例如下图:

image-20211102163900601.png

5.2 同样Lambda不同的函数式接口

有了目标类型概念,同一个表达式就可以通过赋值的方式和不同的函数式接口联系起来,只要它们表达式兼容。如下:

1
2
3
4
5
6
7
8
9
10
java复制代码//示例1:
Callable<Integer> c = () -> 42; //赋值目标对象 Callable<T>
PrivilegedAction<Integer> p = () -> 42;//赋值目标对象 PrivilegedAction<T>
// 示例2:
Comparator<Apple> c1 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
ToIntBiFunction<Apple, Apple> c2 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
BiFunction<Apple, Apple, Integer> c3 =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

5.3 参数类型推断

编译器会从上下文(目标类型)推断出匹配Lambda表达式的函数式接口,也就意味着能推断出Lambda的签名,因此函数描述符可以通过目标类型得到,因此可以进一步省略类型,示例如下:

1
2
3
4
5
6
java复制代码//原始格式
Comparator<Apple> c =
(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//不声明类型,编译器会自动推断出来
Comparator<Apple> c =
(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

5.4 局部变量使用限制

Lambda表达式本质和匿名函数类似,因此使用外部变量时,必须是final类型或者事实上final(lambda使用后,后面不可再变更),否则会编译报错,提示如下:

image-20211102171046259.png

该限制的本质原因:

  • 实例变量存储堆中,局部变量保存在栈上。lambda使用的是其副本,而非原始,变更不可见,会导致误解,因此直接禁止变更(Lambda对值封闭,而不是对变量封闭)。
  • 不鼓励使用改变外部变量的典型命令式编程模式。

6、方法引用

6.1 定义

重复使用方法的定义,并像Lambda一样传递它们。

  • 基础思想:一个Lambda只是描述’直接调用这个方法’,最好名称来调用,而不是去描述如何调用,因为前者可读性更好
  • 基础格式:目标引用放分隔符::前,方法名称放后面。

6.2 构建场景

方法引用主要有三类:

  • 指向静态方法的方法引用:如Integer的parseInt方法,写作Integer::parseInt
  • 任意类型的实例方法引用:如String的length方法,写作String::length
  • 指向现有对象的实例方法:如局部变量exp的getValue方法,可写作exp::getValue

此外,还有有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//示例1:
// Lambda表示式
(args) -> ClassaName.staticMethod(args)
//相应方法引用写法
ClassName::staticMethod

//示例2:
// Lambda表示式(arg0是ClassName类型实例)
(arg0,rest) -> arg0.instanceMethod(rest)
//相应方法引用写法
ClassName::instanceMethod

//示例3:
// Lambda表示式
(args) -> expr.instanceMethod(args)
//相应方法引用写法
expr::instanceMethod

6.2 构造函数引用

可以按构造函数的类型,选择相应的抽象方法来实现。

  • 无参构造器,与Supplier<T>签名()-> T一致。
1
2
3
4
5
6
7
java复制代码//方法引用实现
Supplier<Apple> c1 = Apple::new;
Apple a1 = c1.get()

//等价的Lambda实现
Supplier<Apple> c1 = () -> new Apple();
Apple a1 = c1.get()
  • 一个参数,与Function<T,R>函数描述符 T -> R一致。
1
2
3
4
5
6
7
java复制代码//方法引用实现
Function<Integer,Apple> c1 = Apple::new;
Apple a1 = c1.apply(100);

//等价的Lambda实现
Function<Integer,Apple> c1 = (weight) -> new Apple(weight);
Apple a1 = c1.apply(100);
  • 两个参数,与BiFunction<T,U,R>签名一致
1
2
3
4
5
6
7
java复制代码//方法引用实现
BiFunction<String,Integer,Apple> c1 = Apple::new;
Apple a1 = c1.apply(Color.RED,100);

//等价的Lambda实现
BiFunction<String,Integer,Apple> c1 = (color,weight) -> new Apple(color,weight);
Apple a1 = c1.apply(Color.RED,100);

7、复合Lambda表达式的常见方法

主要是用于将多个简单得Lambda复合成复杂的表达式,主要包含比较器复合、Predicate复合、函数复合。

7.1 比较器的复合

  • 逆序
1
java复制代码inventory.sort(comparing(Apple::getWeight).reversed());
  • 比较器链:主要用于解决多级比较,比如苹果按重量递减排序,相同重量,再按国家排序
1
2
3
4
java复制代码inventory.sort(comparing(Apple::getWeight)
.reversed()
.thenComparing(Apple::getCountry)
);

7.2 Predicate复合

主要有negate-非、and-与、or-或三种复合操作。

1
2
3
4
5
6
7
8
9
java复制代码//红色的苹果
Predicate<Apple> redApple = (a) -> Color.RED.equals(a.getColor());
//非红色的苹果
Predicate<Apple> notRedApple = redApple.negate();
//红色且重量大于150
Predicate<Apple> redAndHeavyApple = redApple.and(a -> a.getWeight() > 150);
//重量>150的红苹果,或者绿苹果
Predicate<Apple> redAndHeavyAppleOrGreen = redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));

7.3 Function复合

Function接口配置了andThencompose两个默认方法,用来将其复合。

  • andThen:先计算自身,再计算参数的函数
  • compose:先计算参数函数,结果作为参数,再计算自身
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码        Function<Integer,Integer> f = (x) -> x+1;
Function<Integer,Integer> g = (x) -> x*2;

//andThen测试
Function<Integer,Integer> h1 = f.andThen(g);
// 结果:4 (先执行f的+1,再执行g的*2)
System.out.println(h1.apply(1));

//compose测试
Function<Integer,Integer> h2 = f.compose(g);
// 结果:3 (先执行g的*2,再执行f的+1)
System.out.println(h2.apply(1));

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%