Lambda 表达式基础 | JavaSE
Lambda 表达式基础
一、学习目标
完成本章后,你应该能够:
- 能够解释 Lambda 表达式是什么,以及它解决了什么代码问题。
- 能够写出 Lambda 表达式的基本语法结构。
- 能够根据函数式接口的抽象方法确定 Lambda 的参数和返回值。
- 能够分别手写无参数、有参数、有返回值和无返回值的 Lambda。
- 能够判断某个匿名内部类能否使用 Lambda 表达式改写。
- 能够理解 Lambda 本身没有独立类型,而是依赖目标函数式接口确定类型。
二、核心知识
2.1 什么是 Lambda 表达式
Lambda 表达式(Lambda Expression)是 Java 8 引入的一种语法形式,用于以更加简洁的方式表示一段可以被传递和执行的行为。
例如,一个函数式接口:
@FunctionalInterface
interface Swim {
void swimming();
}
过去如果需要创建它的实现对象,可以使用匿名内部类:
Swim swim = new Swim() {
@Override
public void swimming() {
System.out.println("游泳");
}
};
使用 Lambda:
Swim swim = () -> {
System.out.println("游泳");
};
可以发现,大量用于描述“我要实现哪个接口、我要重写哪个方法”的模板代码消失了。
Lambda 更关注的是:
这个行为到底要做什么。
而不是:
我要创建一个匿名类,然后实现接口,然后重写方法。
2.2 Lambda 的基本语法
Lambda 最核心的语法结构是:
(参数列表) -> {
方法体
}
也可以理解为:
参数 -> 行为
其中:
左侧:参数
↓
(x, y) -> x + y
↑
行为
箭头:
->
把参数与要执行的行为连接起来。
2.3 Lambda 与函数式接口的关系
Lambda 不能脱离接口独立存在。
例如:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
可以写:
Calculator calculator = (int a, int b) -> {
return a + b;
};
为什么编译器知道:
a
b
是 int?
为什么知道 Lambda 最终需要返回 int?
因为目标类型:
Calculator
已经规定:
int calculate(int a, int b);
因此可以建立对应关系:
Calculator
int calculate(int a, int b)
↓
(int a, int b) -> {
return a + b;
}
Lambda 实际上是在为函数式接口的唯一抽象方法提供实现。
2.4 Lambda 必须具有目标类型
下面的写法是不完整的:
var lambda = (a, b) -> a + b;
编译器无法仅根据 Lambda 自己确定:
a是什么类型?b是什么类型?- 返回值是什么类型?
- Lambda 最终表示什么接口?
因此 Lambda 通常出现在具有明确目标类型的上下文中。
例如赋值:
Calculator calculator = (a, b) -> a + b;
方法参数:
Arrays.sort(students, (a, b) -> Integer.compare(a.getAge(), b.getAge()));
或者显式类型转换:
Calculator calculator =
(Calculator) ((a, b) -> a + b);
对于初学阶段,记住一句话:
Lambda 必须知道自己最终要成为哪个函数式接口的实现。
2.5 Lambda 能替代什么
Lambda 不能替代所有匿名内部类。
它主要用于:
函数式接口的实现。
例如:
interface Fly {
void fly();
}
只有一个抽象方法,因此:
Fly fly = () -> {
System.out.println("飞行");
};
合法。
但是:
interface Player {
void play();
void stop();
}
这里存在两个抽象方法,因此不能写:
Player player = () -> {
System.out.println("...");
};
因为编译器不知道 Lambda 究竟实现:
play()
还是:
stop()
2.6 Lambda 不能直接替代抽象类匿名对象
例如:
abstract class Animal {
public abstract void cry();
}
匿名内部类可以:
Animal animal = new Animal() {
@Override
public void cry() {
System.out.println("喵喵喵");
}
};
但不能改成:
Animal animal = () -> System.out.println("喵喵喵");
因为 Lambda 的目标类型必须是函数式接口类型。
即使这个抽象类只有一个抽象方法,它也不是函数式接口。
所以:
匿名内部类
↓
既可以实现接口
也可以继承抽象类/普通类
Lambda
↓
只能用于函数式接口
这是两者的重要边界。
三、使用方法
3.1 无参数、无返回值
定义接口:
@FunctionalInterface
interface Task {
void run();
}
使用 Lambda:
public class LambdaDemo01 {
public static void main(String[] args) {
Task task = () -> {
System.out.println("执行任务");
};
task.run();
}
}
@FunctionalInterface
interface Task {
void run();
}
对应关系:
void run()
↓
() -> {
System.out.println("执行任务");
}
没有参数时,必须保留:
()
3.2 一个参数、无返回值
接口:
@FunctionalInterface
interface Printer {
void print(String message);
}
Lambda:
Printer printer = (String message) -> {
System.out.println(message);
};
调用:
printer.print("星雨笔录");
完整程序:
public class LambdaDemo02 {
public static void main(String[] args) {
Printer printer = (String message) -> {
System.out.println(message);
};
printer.print("星雨笔录");
}
}
@FunctionalInterface
interface Printer {
void print(String message);
}
3.3 多参数、无返回值
接口:
@FunctionalInterface
interface MessagePrinter {
void print(String name, int age);
}
Lambda:
MessagePrinter printer = (String name, int age) -> {
System.out.println("姓名:" + name);
System.out.println("年龄:" + age);
};
调用:
printer.print("LingXi", 21);
注意:
Lambda 参数的数量必须与函数式接口抽象方法保持一致。
3.4 无参数、有返回值
接口:
@FunctionalInterface
interface NumberGenerator {
int generate();
}
Lambda:
NumberGenerator generator = () -> {
return 100;
};
调用:
int number = generator.generate();
System.out.println(number);
3.5 多参数、有返回值
这是最常见的形式之一。
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
使用:
public class LambdaDemo03 {
public static void main(String[] args) {
Calculator add = (int a, int b) -> {
return a + b;
};
Calculator subtract = (int a, int b) -> {
return a - b;
};
System.out.println(add.calculate(10, 20));
System.out.println(subtract.calculate(10, 20));
}
}
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
这里最重要的不是“Lambda 是一种奇怪的新方法”。
而是:
Calculator
↓
规定行为规格
Lambda
↓
提供行为实现
所以可以为同一个函数式接口提供不同实现:
Calculator add = (a, b) -> a + b;
Calculator subtract = (a, b) -> a - b;
Calculator multiply = (a, b) -> a * b;
这已经体现出函数式编程的重要思想:
行为也可以像数据一样被保存、传递和选择。
3.6 Lambda 作为方法参数
Lambda 真正有价值的地方之一,就是能够直接传递行为。
例如:
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
定义方法:
public static void calculate(
int a,
int b,
Calculator calculator
) {
int result = calculator.calculate(a, b);
System.out.println(result);
}
调用:
calculate(10, 20, (a, b) -> a + b);
calculate(10, 20, (a, b) -> a * b);
完整程序:
public class LambdaDemo04 {
public static void main(String[] args) {
calculate(10, 20, (a, b) -> a + b);
calculate(10, 20, (a, b) -> a - b);
calculate(10, 20, (a, b) -> a * b);
}
public static void calculate(
int a,
int b,
Calculator calculator
) {
int result = calculator.calculate(a, b);
System.out.println(result);
}
}
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
此时:
(a, b) -> a + b
本质上就是作为参数被传入方法的一段行为。
这种思维在后面的:
Comparator- 集合
forEach - Stream
- 线程
- 事件监听
中会大量出现。
四、原理与进阶
4.1 Lambda 不是“没有名字的方法”
初学时可以把 Lambda 暂时理解成:
一段没有显式方法名的行为代码。
但从 Java 类型系统来看,更准确的理解是:
Lambda 表达式在函数式接口目标类型的上下文中,用于提供该接口函数方法的实现。
例如:
Calculator add = (a, b) -> a + b;
(a, b) -> a + b 自己并不能说明完整类型。
真正使它成立的是:
Calculator
4.2 Lambda 是多态表达式
Lambda 的具体类型受到上下文影响。
例如:
@FunctionalInterface
interface A {
int calculate(int a, int b);
}
@FunctionalInterface
interface B {
int calculate(int a, int b);
}
同样:
(a, b) -> a + b
既可能成为:
A
的实现,也可能成为:
B
的实现。
例如:
A a = (x, y) -> x + y;
B b = (x, y) -> x + y;
因此 Lambda 并不是看到:
(x, y) -> x + y
以后就拥有一个固定的、独立的类类型。
它依赖上下文提供目标类型。
4.3 创建 Lambda 与执行 Lambda 是两个阶段
例如:
Runnable task = () -> {
System.out.println("Hello Lambda");
};
执行到这一行时,并不意味着:
System.out.println("Hello Lambda");
马上执行。
真正执行发生在:
task.run();
因此:
创建 Lambda
↓
获得函数式接口实例
调用函数式接口方法
↓
执行 Lambda 方法体
不要把:
Runnable task = () -> ...
理解成“立即执行右侧代码”。
五、实践应用
5.1 集合遍历
例如:
List<String> names =
List.of("Java", "MySQL", "Spring");
传统增强 for:
for (String name : names) {
System.out.println(name);
}
Lambda 风格:
names.forEach(name -> {
System.out.println(name);
});
这里:
name -> {
System.out.println(name);
}
描述的是:
对每一个元素执行什么行为。
5.2 排序规则
过去:
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return Integer.compare(
o1.getAge(),
o2.getAge()
);
}
});
Lambda:
Arrays.sort(
students,
(o1, o2) -> Integer.compare(
o1.getAge(),
o2.getAge()
)
);
Lambda 将我们的注意力从:
创建 Comparator 匿名内部类
→ 重写 compare()
→ 编写比较规则
缩短为:
直接提供比较规则
5.3 线程任务
传统匿名内部类:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程任务");
}
});
Lambda:
Thread thread = new Thread(() -> {
System.out.println("执行线程任务");
});
这里 Thread 构造器需要 Runnable,而:
Runnable
是函数式接口,因此可以传入 Lambda。
这个例子以后学习多线程时还会再次遇到。
六、常见问题
6.1 Lambda 可以替代所有匿名内部类吗?
不能。
Lambda 的目标必须是函数式接口。
例如:
interface A {
void test1();
void test2();
}
存在两个抽象方法,不能使用 Lambda。
6.2 一个抽象类只有一个抽象方法,可以使用 Lambda 吗?
不能。
abstract class Animal {
abstract void cry();
}
虽然只有一个抽象方法,但它不是接口,因此不是函数式接口。
6.3 Lambda 是一个普通方法吗?
不是。
例如:
(a, b) -> a + b
没有普通 Java 方法声明中的:
- 方法名
- 返回值类型声明
- 修饰符
Lambda 是一种表达式。
6.4 为什么 Lambda 参数类型可以由编译器知道?
因为函数式接口已经规定参数类型。
例如:
interface Calculator {
int calculate(int a, int b);
}
因此:
Calculator c = (a, b) -> a + b;
中的:
a
b
可以根据目标接口推断为 int。
详细省略规则将在下一章专门学习。
6.5 Lambda 创建以后会马上执行吗?
不会。
Runnable task =
() -> System.out.println("执行");
这里只是创建对应的函数式接口实例。
调用:
task.run();
时 Lambda 方法体才真正执行。
6.6 为什么下面代码无法确定类型?
// 错误示例
// var operation = (a, b) -> a + b;
因为 Lambda 需要目标函数式接口类型。
编译器缺少用于确定参数和返回值类型的目标类型。
七、练习与验收
7.1 知识问答
- 什么是 Lambda 表达式?
- Lambda 从哪个 Java 版本开始正式引入?
- Lambda 主要解决了哪类代码冗余问题?
- Lambda 的基本语法结构是什么?
- Lambda 为什么必须与函数式接口配合使用?
- 什么叫 Lambda 的目标类型?
- Lambda 可以替代任意匿名内部类吗?为什么?
- 一个只有一个抽象方法的抽象类为什么仍然不能直接使用 Lambda?
- Lambda 创建与 Lambda 方法体执行是否是同一件事?
- 为什么
(a, b) -> a + b不能脱离上下文确定完整类型?
7.2 代码阅读
阅读:
@FunctionalInterface
interface Operation {
int execute(int a, int b);
}
public class Test {
public static void main(String[] args) {
Operation operation = (int a, int b) -> {
return a * b;
};
System.out.println(operation.execute(4, 5));
}
}
回答:
- Lambda 对应的是哪个抽象方法?
a和b分别是什么类型?- Lambda 方法体什么时候真正执行?
- 最终输出什么?
- 如果
Operation再增加一个抽象方法,该程序会发生什么?
阅读:
@FunctionalInterface
interface Task {
void execute();
}
public class Test {
public static void main(String[] args) {
Task task = () -> {
System.out.println("A");
};
System.out.println("B");
task.execute();
}
}
禁止运行程序,判断完整输出顺序,并解释原因。
7.3 手写代码
- 定义函数式接口
Printer:
void print(String text);
使用 Lambda 输出字符串。
- 定义:
interface Calculator {
int calculate(int a, int b);
}
分别使用 Lambda 实现:
- 加法
- 减法
- 乘法
- 除法
- 定义函数式接口:
interface Check {
boolean test(int number);
}
分别实现:
- 判断偶数
- 判断正数
- 判断是否大于 100
- 定义一个方法:
execute(int a, int b, Calculator calculator)
将计算规则作为参数传入。
7.4 Debug
下面代码有什么问题?
interface Action {
void start();
void stop();
}
public class Test {
public static void main(String[] args) {
Action action =
() -> System.out.println("start");
}
}
要求:
- 判断问题发生在编译期还是运行期。
- 说明错误原因。
- 给出至少一种合理的接口设计修复方案。
判断:
abstract class Animal {
abstract void cry();
}
public class Test {
public static void main(String[] args) {
Animal animal =
() -> System.out.println("喵");
}
}
说明为什么不能编译。
7.5 综合训练
设计一个简易计算器。
要求:
Calculator add
Calculator subtract
Calculator multiply
Calculator divide
全部使用 Lambda 实现。
统一通过:
calculate(
double a,
double b,
Calculator calculator
)
执行。
要求思考:
- 为什么这里不需要创建四个
Calculator实现类? - Lambda 在这个程序中究竟代表“数据”还是“行为”?
- 如果未来增加取最大值操作,需要修改
Calculator接口吗?
7.6 本章验收
闭卷完成以下任务:
- 能用一句话解释 Lambda。
- 能写出 Lambda 基本语法。
- 能解释 Lambda 与函数式接口的关系。
- 能判断一个接口能否使用 Lambda。
- 能分别写出无参数、有参数、有返回值、无返回值 Lambda。
- 能把简单函数式接口匿名内部类改成 Lambda。
- 能解释为什么 Lambda 必须存在目标类型。
如果以上内容仍需要频繁查看语法模板,则本章暂时不能认为完全掌握。