☕ 第四章 面向对象(下)

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 的规矩推一遍:

判定练习:

接口 抽象方法数 是函数式接口?
Flyable(只有 fly()) 1 ✅ 是
4.6 那个有 fly() + land() 的 2 ❌ 不是
Comparator(只算 compare 一个) 1 ✅ 是
Runnable(只算 run 一个) 1 ✅ 是

@FunctionalInterface:给编译器装的警报器

@FunctionalInterface
interface Calculator {
    int calc(int a, int b);
}

这个注解不是语法要求——不加它,只要"只有一个抽象方法",照样是函数式接口。它的作用是请编译器帮你看门:谁往接口里加了第二个抽象方法,立刻编译报错。建议自定义函数式接口时都加上,防手滑。

📝 Java 自带四个常用函数式接口(混脸熟即可)

Supplier<T>(无参有返回——生产者)、Consumer<T>(有参无返回——消费者)、Function<T,R>(有参有返回——加工厂)、Predicate<T>(出 boolean——裁判)。现在不用背,学到集合和 Stream 时天天见面。

3. Lambda 三段式语法

(参数列表) -> { 方法体 }

三段拆解:

  1. 参数列表:照抄那个抽象方法的参数——类型可省,变量名不能省;
  2. ->:箭头,固定写法,读作"传入……执行……";
  3. 方法体:就是普通方法体,该干嘛干嘛。

演化三步(对着 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 的"编译看左运行看右"原样适用。

三条省略规则

  1. 参数类型可省:编译器从左边接口的方法签名里推出来(又是"编译看左");
  2. 只有一个参数时,小括号可省:s -> s.length();
  3. 方法体只有一句时,大括号、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 秒实验

  1. 警报器生效:给 Calculator 加上 @FunctionalInterface,再塞第二个抽象方法 int calc2(int a);——立刻编译报错"multiple non-overriding abstract methods"。
  2. 类型省一半不行:把 (a, b) 改成 (int a, b)——编译报错。类型要省全省、要写全写。
  3. 多语句不能省花括号:方法体写成两条语句但去掉大括号——报错;补回 {} 和分号就好了。

6. 常见误区

⚠️ 误区 1:Lambda 能完全替代匿名内部类

它只认函数式接口。接口有两个以上抽象方法、或者要 new 抽象类/普通类,还是得匿名内部类上场。俩是搭档,不是替换。

⚠️ 误区 2:Lambda 能给抽象类用

不能。Lambda 只支持接口——抽象类"不能 new 且可能有构造/属性",Lambda 的语法推不出这些信息。Runnable r = () -> ... 行,抽象类不行。

⚠️ 误区 3:省略规则可以只省一半

单语句省略时,大括号、return、分号是一个套餐:要省仨一起省,要留仨一起留。(a, b) -> return a + b; 这种半省不省的写法直接编译报错。

⚠️ 误区 4:不加 @FunctionalInterface 就不是函数式接口

注解只是给编译器的检查器。判定标准始终是"有且只有一个抽象方法"这个事实本身,注解加不加都一样。

⚠️ 误区 5:Lambda 改变了多态规则

没变。Calculator add = (a, b) -> a + b; 本质还是"左边接口引用、右边实现类对象"——编译器在幕后造了个类实现 Calculator,和 4.6 匿名内部类同一个原理,只是不让你写仪式了。

7. 自测题(先自己答,再展开)

❓ Q1:什么样的接口才能用 Lambda?两个抽象方法的接口怎么办?

有且只有一个抽象方法的接口(函数式接口)。两个抽象方法的接口编译器不知道 () 对应谁,Lambda 用不了——只能匿名内部类老老实实全兑现。

❓ Q2:`(a, b) -> a + b` 里 a、b 的类型从哪来?为什么敢省?

从左边接口变量的方法签名里推出来:Calculator.calc(int a, int b) 早就写明了类型。"编译看左"的老口诀又一次生效——左边定规矩,右边填内容。

❓ Q3:把 `(a, b) -> a + b` 写成 `(a, b) -> { a + b }` 行不行?

不行。一旦加大括号,就进入"普通方法体"模式:有返回值的方法必须写 return,且分号不能省。正确写法是 -> { return a + b; }(仨都留)或 -> a + b(仨都省)。

❓ Q4:Runnable 和 Comparator 是不是函数式接口?凭什么?

都是。Runnable 只有 run() 一个抽象方法;Comparator 看着方法多,但多数是 default/static 方法,抽象方法只有 compare() 一个——default/static 有实现,不占名额。

❓ Q5(毕业题):把 4.6 的匿名 Flyable 改写成 Lambda,并说出 Lambda 帮不了你的场景
Flyable f = () -> System.out.println("扑棱扑棱");

Lambda 帮不了的场景:① 接口有 2 个以上抽象方法(比如加了 land() 的 Flyable);② 需要实现的是抽象类/普通类;③ 实现逻辑很长、需要持有状态、要单独测试——这时匿名内部类甚至正经类更合适。

8. 下一步学什么


相关笔记