4.7 函数式编程
函数式编程 = 把"一段代码"当数据传来传去。它是 4.6 匿名内部类的瘦身版:既然接口只要一个方法,那 new、方法名、@Override 全是仪式——Lambda 表达式把五行压成一行。入场券只有一张:接口必须是"函数式接口"(有且只有一个抽象方法)。
1. 动机:匿名内部类的最后一斤赘肉
4.6 结尾埋的伏笔,现在收网。回头看那段匿名内部类:
Flyable f = new Flyable() { // 编译器知道:左边是 Flyable
@Override // 编译器知道:要实现接口方法
public void fly() { // 编译器知道:方法名就是 fly
System.out.println("扑棱扑棱");
}
};
真正有信息量的只有方法体那一行——其余全是编译器早就知道的重复劳动。既然你全知道,为什么不让我直接把那一行交给你?于是:
Flyable f = () -> System.out.println("扑棱扑棱");
五行变一行,效果一模一样。这就是 Lambda 表达式,也是"函数式编程"在 Java 里的入口:行为(一段代码)可以像值一样,赋给变量、当参数传来传去。2.6 学过"方法 = 把一段代码打包",Lambda 再进一步——把打包好的代码当数据用。
"函数式编程"听起来高深,在 Java 入门阶段它就三件事:函数式接口、Lambda 写法、什么时候能用。别被名词吓住,下面一节节拆。
2. 函数式接口:Lambda 的入场券
定义一句话:有且只有一个抽象方法的接口,叫函数式接口(Functional Interface)。
对照 4.3 的规矩推一遍:
- 接口里的抽象方法自动是
public abstract——4.3 学过,不用手写; - 只要抽象方法恰好是一个,它就是函数式接口,跟叫什么名字无关;
- 接口里的
default方法、static方法不占名额(它们有实现,不是抽象方法——会认即可)。
判定练习:
| 接口 | 抽象方法数 | 是函数式接口? |
|---|---|---|
| Flyable(只有 fly()) | 1 | ✅ 是 |
| 4.6 那个有 fly() + land() 的 | 2 | ❌ 不是 |
| Comparator(只算 compare 一个) | 1 | ✅ 是 |
| Runnable(只算 run 一个) | 1 | ✅ 是 |
@FunctionalInterface:给编译器装的警报器
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
这个注解不是语法要求——不加它,只要"只有一个抽象方法",照样是函数式接口。它的作用是请编译器帮你看门:谁往接口里加了第二个抽象方法,立刻编译报错。建议自定义函数式接口时都加上,防手滑。
Supplier<T>(无参有返回——生产者)、Consumer<T>(有参无返回——消费者)、Function<T,R>(有参有返回——加工厂)、Predicate<T>(出 boolean——裁判)。现在不用背,学到集合和 Stream 时天天见面。
3. Lambda 三段式语法
(参数列表) -> { 方法体 }
三段拆解:
- 参数列表:照抄那个抽象方法的参数——类型可省,变量名不能省;
->:箭头,固定写法,读作"传入……执行……";- 方法体:就是普通方法体,该干嘛干嘛。
演化三步(对着 4.6 的代码看)
// 第 0 步:匿名内部类(4.6 的写法)
Flyable f = new Flyable() {
@Override
public void fly() {
System.out.println("扑棱扑棱");
}
};
// 第 1 步:换成 Lambda —— new、方法名、@Override 全部蒸发
Flyable f = () -> {
System.out.println("扑棱扑棱");
};
// 第 2 步:单语句,大括号也省了
Flyable f = () -> System.out.println("扑棱扑棱");
带参数的例子(本节主角场景)
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
public class TestLambda {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b; // 一行实现"加法"
Calculator mul = (a, b) -> a * b; // 一行实现"乘法"
System.out.println(add.calc(3, 4)); // 7
System.out.println(mul.calc(3, 4)); // 12
}
}
同一个接口,Lambda 一换,calc 就换了一种行为——行为像值一样被赋给了变量,这就是"函数式"的味道。多态的老底子还在:Calculator add 左边接口右边实现,4.4 的"编译看左运行看右"原样适用。
三条省略规则
- 参数类型可省:编译器从左边接口的方法签名里推出来(又是"编译看左");
- 只有一个参数时,小括号可省:
s -> s.length(); - 方法体只有一句时,大括号、return、分号可省——注意是仨一起省:省了
{}就不能写return和;;要写return就仨都留着。
Calculator add = (int a, int b) -> { return a + b; }; // 全写:合法但啰嗦
Calculator add2 = (a, b) -> a + b; // 全省:推荐
4. Lambda vs 匿名内部类:什么时候用哪个
| 维度 | 匿名内部类 | Lambda |
|---|---|---|
| 能用的地方 | 接口、抽象类、普通类都行 | 只能函数式接口 |
| 能实现的方法数 | 多个(必须全兑现) | 只能那一个 |
| 代码量 | 五行起步 | 一行 |
| 推荐场景 | 方法多、逻辑长、要持有状态 | 单方法接口 + 短逻辑(实际开发 99% 场景) |
Lambda 的两大实战主场(现在混脸熟,后面章节天天用):
// ① 开线程:Runnable 正好只有一个 run()
new Thread(() -> System.out.println("子线程在干活")).start();
// ② 排序:按字符串长度排,Comparator 正好只有一个 compare()
String[] names = {"波仔", "墨池", "Java", "AI"};
Arrays.sort(names, (s1, s2) -> s1.length() - s2.length());
System.out::println 这种双冒号写法叫方法引用,是 Lambda 的"再瘦一层"——这句想表达"把拿到的每个东西都交给 System.out 的 println 干"。现在见到认识就行,常用 API / 集合章再正式学。
5. 动手:三种 Lambda
在 IDEA04 里新建 TestLambda(Calculator 照第 3 节抄,Flyable 用 4.3/4.6 的):
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
public class TestLambda {
public static void main(String[] args) {
// ① 自定义函数式接口 + Lambda
Calculator add = (a, b) -> a + b;
System.out.println(add.calc(3, 4)); // 7
// ② 给 4.6 的匿名 Flyable 换装
Flyable plane = () -> System.out.println("飞机起飞");
plane.fly();
// ③ 实战主场:开线程
new Thread(() -> System.out.println("子线程在干活")).start();
}
}
成功标准:三段各出对应输出;把 add 换成 mul 输出跟着变——体会"同一个接口变量,行为随 Lambda 而变"。
三个 30 秒实验
- 警报器生效:给 Calculator 加上 @FunctionalInterface,再塞第二个抽象方法
int calc2(int a);——立刻编译报错"multiple non-overriding abstract methods"。 - 类型省一半不行:把
(a, b)改成(int a, b)——编译报错。类型要省全省、要写全写。 - 多语句不能省花括号:方法体写成两条语句但去掉大括号——报错;补回
{}和分号就好了。
6. 常见误区
它只认函数式接口。接口有两个以上抽象方法、或者要 new 抽象类/普通类,还是得匿名内部类上场。俩是搭档,不是替换。
不能。Lambda 只支持接口——抽象类"不能 new 且可能有构造/属性",Lambda 的语法推不出这些信息。Runnable r = () -> ... 行,抽象类不行。
单语句省略时,大括号、return、分号是一个套餐:要省仨一起省,要留仨一起留。(a, b) -> return a + b; 这种半省不省的写法直接编译报错。
注解只是给编译器的检查器。判定标准始终是"有且只有一个抽象方法"这个事实本身,注解加不加都一样。
没变。Calculator add = (a, b) -> a + b; 本质还是"左边接口引用、右边实现类对象"——编译器在幕后造了个类实现 Calculator,和 4.6 匿名内部类同一个原理,只是不让你写仪式了。
7. 自测题(先自己答,再展开)
有且只有一个抽象方法的接口(函数式接口)。两个抽象方法的接口编译器不知道 () 对应谁,Lambda 用不了——只能匿名内部类老老实实全兑现。
从左边接口变量的方法签名里推出来:Calculator.calc(int a, int b) 早就写明了类型。"编译看左"的老口诀又一次生效——左边定规矩,右边填内容。
不行。一旦加大括号,就进入"普通方法体"模式:有返回值的方法必须写 return,且分号不能省。正确写法是 -> { return a + b; }(仨都留)或 -> a + b(仨都省)。
都是。Runnable 只有 run() 一个抽象方法;Comparator 看着方法多,但多数是 default/static 方法,抽象方法只有 compare() 一个——default/static 有实现,不占名额。
Flyable f = () -> System.out.println("扑棱扑棱");
Lambda 帮不了的场景:① 接口有 2 个以上抽象方法(比如加了 land() 的 Flyable);② 需要实现的是抽象类/普通类;③ 实现逻辑很长、需要持有状态、要单独测试——这时匿名内部类甚至正经类更合适。
8. 下一步学什么
- ☑ 4.8 本章小结 ✅ —— 七节串线、概念速查 14 条、代码模板 4 段、七大名坑、三大特性收官、毕业大题。第四章完结。点此复习
相关笔记
- 4.6 内部类 ← 上一篇:Lambda 是匿名内部类的瘦身版,伏笔在此收网
- 4.3 抽象类和接口 ← 函数式接口 = 只有一项条款的合同
- 4.4 多态 ← 接口变量 = Lambda 对象,编译看左运行看右照旧
- 2.6 方法 ← Lambda 的参数与返回值规则全从方法定义推
- 📇 Java基础 · 索引
- 🏠 返回主页