21.Java基础之Lambda表达式
江头未是风波恶,别有人间行路难!
——辛弃疾《鹧鸪天》
JDK8新特性—Lambda表达式
1. 函数式编程思想概述
在数学中,
函数就是有输入量、输出量的一套计算方案
,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”
,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。
1.1 冗余的Runnable代码
1.2 TreeSet的定制排序
1.3 为什么使用Lambda表达式呢?
Lambda 是一个 匿名函数,我们可以把 Lambda 表达式理解为是 一段可以传递的代码(将代码像数据一样进行传递)。使用它可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
2. Lambda表达式语法☆
Lambda 表达式:
在Java 8 语言中引入的一种新的语法元素和操作符
。这个操作符为 “->”
, 该操作符被称为Lambda 操作符 或 箭头操作符
。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的
参数列表
。无参数则留空,多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
右侧:指定了 Lambda 体,是抽象方法的实现逻辑,也即Lambda 表达式要执行的功能。
~~~~java
// 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
![lambdasss_看图王](https://oss-blogs.oss-cn-hangzhou.aliyuncs.com/blogs/itbuild/JavaSE/lambdasss_看图王-1605322100982.jpg)
### 2.1 类型推断
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的`“类型推断”`。
![image-20201114105033219](https://oss-blogs.oss-cn-hangzhou.aliyuncs.com/blogs/itbuild/JavaSE/image-20201114105033219.png)
### 2.2 Lambda表达式练习
##### 体验Lambda的更优写法
借助Java 8的全新语法, `Runnable` 接口的匿名内部类写法可以通过更简单的`Lambda`表达式达到等效:
~~~java
public class DemoLambdaRunnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
/`
* 覆盖重写抽象方法
*/
@Override
public void run() {
System.out.println("多线程任务执行!");
}
};
Thread thread = new Thread(task);
// 启动线程
thread.start();
// ||
// ||
new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
~~~
1. 从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。
2. 不再有`“不得不创建接口对象”`的束缚,不再有`“抽象方法覆盖重写”`的负担,就是这么简单!
##### 使用Lambda标准格式(无参无返回)
给定一个厨子 `Cook` 接口,内含唯一的抽象方法` makeFood` ,且无参数、无返回值。
~~~java
/`
* @Date 2020/11/14 14:14
* @Version 10.21
* @Author DuanChaojie
*/
@FunctionalInterface
public interface Cook {
void makeFood();
}
~~~
在下面的代码中,请使用Lambda的`标准格式`调用 `invokeCook 方法`,打印输出“吃饭啦!”字样:
~~~java
/`
* @Date 2020/11/14 14:15
* @Version 10.21
* @Author DuanChaojie
*/
public class InvokeCook {
public static void main(String[] args) {
// TODO 请在此使用Lambda【标准格式】调用invokeCook方法
invokeCook(()->
System.out.println("吃饭了!")
);
}
private static void invokeCook(Cook cook){
cook.makeFood();
}
}
~~~
`小括号代表 Cook 接口 makeFood 抽象方法的参数为空,大括号代表 makeFood 的方法体。`
##### Lambda的参数和返回值
下面举例演示` java.util.Comparator<T>` 接口的使用场景代码,其中的抽象方法定义为:`public abstract int compare(T o1, T o2);`当需要对一个对象数组进行排序时, `Arrays.sort` 方法需要一个 `Comparator` 接口实例来指定排序的规则。假设有一个 `Person `类,含有 `String name` 和` int age` 两个成员变量:
~~~java
package cn.justweb.lambda;
/`
* @Date 2020/11/14 14:22
* @Version 10.21
* @Author DuanChaojie
*/
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
~~~
###### Lambda写法
~~~java
/`
* @Date 2020/11/14 14:23
* @Version 10.21
* @Author DuanChaojie
*/
public class ComparatorLambda {
public static void main(String[] args) {
Person[] arr = {new Person("古力娜扎",19), new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) };
// 降序
Arrays.sort(arr,(Person p1, Person p2) -> p2.getAge() - p1.getAge());
for (Person person: arr){
System.out.println(person);
}
}
}
~~~
##### 使用Lambda标准格式(有参有返回)
给定一个计算器 `Calculator` 接口,内含抽象方法` calc` 可以将两个int数字相乘得到和值:
~~~java
/`
* @Date 2020/11/14 14:31
* @Version 10.21
* @Author DuanChaojie
*/
@FunctionalInterface
public interface Calculator {
int calc(int a,int b);
}
~~~
在下面的代码中,请使用Lambda的`标准格式`调用` invokeCalc` 方法,完成5和6的相乘计
~~~java
/`
* @Date 2020/11/14 14:31
* @Version 10.21
* @Author DuanChaojie
*/
public class InvokeCalc {
public static void main(String[] args) {
int result = invokeCalc(5, 6, (int a, int b) -> a * b);
// result = 30
System.out.println("result = " + result);
}
public static int invokeCalc(int a,int b,Calculator calc){
return calc.calc(a,b);
}
}`
~~~
小括号代表 `Calculator` 接口` calc` 抽象方法的参数,大括号代表 `calc` 的方法体。
### 2.3 Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
1. `小括号内参数的类型可以省略;`
2. 如果小括号内`有且仅有一个参`,则小括号可以省略;
3. 如果大括号内`有且仅有一个语句`,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
备注:掌握这些省略规则后,请对应地回顾本章开头的多线程案例。
### 2.4 Lambda的使用前提
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
1. 使用Lambda必须具有接口,且要求`接口中有且仅有一个抽象方法`。无论是JDK内置的 `Runnable` 、 `Comparator` 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
2. 使用Lambda必须具有`上下文推断`。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
3. 备注:有且仅有一个抽象方法的接口,称为`“函数式接口”`。
## 3. 函数式接口
1. ==只包含一个抽象方法的接口,称为 函数式接口。==
2. 你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常(即:非运行时异常),那么该异常需要在目标接口的抽象方法上进行声明)。
3. 我们可以在一个接口上使用 `@FunctionalInterface 注解`,这样做可以检查它是否是一个函数式接口。同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
4. `在java.util.function包下定义了Java 8 的丰富的函数式接口。`
![image-20201114112806531](https://oss-blogs.oss-cn-hangzhou.aliyuncs.com/blogs/itbuild/JavaSE/image-20201114112806531.png)
### 3.1 如何理解函数式接口?
1. Java从诞生日起就是一直倡导“一切皆对象”,在Java里面面向对象(OOP)编程是一切。但是随着python、scala等语言的兴起和新技术的挑战,Java不得不做出调整以便支持更加广泛的技术要求,也`即java不但可以支持OOP还可以支持OOF(面向函数编程)`
2. 在函数式编程语言当中,函数被当做一等公民对待。==在将函数作为一等公民的编程语言中,Lambda表达式的类型是函数。==但是在Java8中,有所不同。`在Java8中,Lambda表达式是对象,而不是函数,它们必须依附于一类特别的对象类型——函数式接口。`
3. 简单的说,在Java8中,Lambda表达式就是一个函数式接口的实例。这就是Lambda表达式和函数式接口的关系。也就是说,只要一个对象是函数式接口的实例,那么该对象就可以用Lambda表达式来表示。
4. `所以以前用匿名实现类表示的现在都可以用Lambda表达式来写。`
### 3.2 函数式接口举例
~~~~java
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
3.3 自定义函数式接口
MyFunction
1 | /` |
MyFunctionTest
1 | /` |
3.4 Java内置四大核心函数式接口
Supplier接口
java.util.function.Supplier<T>
接口仅包含一个无参的方法: T get()
。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供
”一个符合泛型类型的对象数据。
1 | /** |
练习:求数组元素最大值:
- 使用
Supplier
接口作为方法参数类型,通过Lambda
表达式求出int
数组中的最大值。提示:接口的泛型请使用java.lang.Integer
类。
1 | /** |
Consumer接口
java.util.function.Consumer<T>
接口则正好相反,它不是生产一个数据,而是消费
一个数据,其数据类型由泛型参数决定。Consumer 接口中包含抽象方法
void accept(T t)
,意为消费一个指定泛型的数据。基本使用如:
1 | /** |
Consumer接口默认方法andThen
如果一个方法的参数和返回值全都是
Consumer
类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer
接口中的default
方法andThen
。下面是JDK8 Consumer类
的源代码:
1 |
|
java.util.Objects 的 requireNonNull 静态方法
将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。- 要想实现组合,需要两个或多个Lambda表达式即可,
而 andThen 的语义正是“一步接一步”操作
。例如两个步骤组合的情况:
1 | /** |
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。
练习:格式化打印信息
下面的字符串数组当中存有多条信息,请按照格式
“ 姓名:XX 性别:XX”
的格式将信息打印出来。要求将打印姓名的动作作为第一个Consumer
接口的Lambda
实例,将打印性别的动作作为第二个Consumer
接口的Lambda
实例,将两个Consumer
接口按照顺序“拼接”到一起。
1 | /** |
4. 方法引用和构造器引用
Employee
1 | public class Employee { |
4.1 方法引用
- 使用场景:当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!
- 方法引用可以看做是Lambda表达式深层次的表达。换句话说,方法引用就是Lambda表达式,也就是函数式接口的一个实例,通过方法的名字来指向一个方法,
可以认为是Lambda表达式的一个语法糖
。- 要求:
实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!
- 格式:使用
操作符 “::” 将类(或对象) 与 方法名分隔开来
。如下三种主要使用情况:
对象:: 实例方法名
类:: 静态方法名
类:: 实例方法名
1 | /** |
4.2 构造器引用
- 构造器引用:
格式: ClassName::new
- 与函数式接口相结合,自动与函数式接口中方法兼容。
- 可以把构造器引用赋值给定义的方法,要求构造器参数列表要与接口中抽象方法的参数列表一致!且方法的返回值即为构造器对应类的对象。
- 数组引用:
格式: type[] :: new
1 | /** |