Lambda与函数式接口
本篇定位
JDK 8 最重要的语言级变更:把"代码作为数据"带进 Java。Lambda 是写法,函数式接口是它赖以落地的类型系统,方法引用是 Lambda 的简写。学会它,Stream API(08-Stream与Optional)、回调、策略模式全部换一种写法。
一、问题提出:行为参数化的老写法有多啰嗦
给"排序时按什么规则比"传一段逻辑,JDK 8 之前只能匿名内部类:
List<String> names = new ArrayList<>(List.of("Charlie", "alice", "Bob"));
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareToIgnoreCase(b);
}
});
五行里真正有信息量的只有一行。匿名内部类的痛点:语法噪声淹没业务语义。Lambda 把它压成一行:
names.sort((a, b) -> a.compareToIgnoreCase(b));
二、Lambda 语法:参数 → 箭头 → 体
(参数列表) -> { 语句; } // 标准形态
() -> expression // 无参数、体是表达式(表达式的值就是返回值)
x -> x * 2 // 单参数可省括号
(a, b) -> a + b // 体是表达式时省略 return 和分号
三条语法要点:
- 参数类型可省——编译器从目标类型推断(见下节"函数式接口");
- 体是单个表达式时可省大括号与 return;多条语句必须
{}且显式 return; - Lambda 是表达式不是对象——它没有独立的类型,类型由"赋值目标/参数位置"决定(这一点和匿名内部类有本质区别)。
变量捕获规则(面试高频,内部类同源问题):Lambda 可以引用外部的局部变量,但该变量必须是 final 或事实最终(effectively final)——
int base = 10; // 没写 final,但之后没改过 → effectively final
Function<Integer, Integer> f = x -> x + base; // ✅
base = 20; // ❌ 一旦改了,上面的 f 编译报错
原因:Lambda 捕获的是变量的值副本(存在合成字段里),允许改会造成"两份变量各改各的"语义混乱。想跨 Lambda 累加就用数组、AtomicInteger 或改用 Stream 归约。
三、函数式接口:Lambda 的类型载体
只有一个抽象方法的接口就是函数式接口(@FunctionalInterface 注解只做编译期校验,不加也行,加了编译器帮你把关):
@FunctionalInterface
public interface Calculator {
int calc(int a, int b); // 唯一抽象方法
}
Calculator add = (a, b) -> a + b; // Lambda 赋给它,类型才成立
Calculator mul = (a, b) -> a * b;
int r = add.calc(3, 4); // 7
接口里有没有别的方法不影响"函数式"判定——default 方法和 static 方法不算抽象方法(这正是 JDK 8 给接口加 default 的深层动机:让 Comparator、Runnable 这些老接口能携带公共逻辑而不破坏"函数式"资格,也更方便 JDK 自己演化)。
JDK 内置四大函数式接口——绝大多数场景不用自己定义:
| 接口 | 抽象方法 | 语义 | 典型场景 |
|---|---|---|---|
Function<T,R> |
R apply(T t) |
进 T 出 R | 映射转换 |
Predicate<T> |
boolean test(T t) |
断言真假 | 过滤条件 |
Consumer<T> |
void accept(T t) |
消费不返回 | foreach 副作用 |
Supplier<T> |
T get() |
无中生有 | 工厂/懒加载 |
加上带原始类型的变体(IntPredicate 避免装箱)、双参变体(BiFunction)、二元运算(BinaryOperator<T>:两个 T 进一个 T 出)——java.util.function 包 40+ 接口都是这四大件的排列组合。
四、方法引用:Lambda 的"高阶简写"
当 Lambda 体只是"调用一个已存在的方法"时,可以进一步简写为方法引用:
names.forEach(s -> System.out.println(s)); // Lambda
names.forEach(System.out::println); // 方法引用:类名::实例方法
Function<String, Integer> len = String::length; // s -> s.length()
Supplier<List<String>> maker = ArrayList::new; // 构造器引用
Function<Integer, int[]> arrMaker = int[]::new; // 数组构造引用
BiFunction<String, String, Boolean> starts = String::startsWith; // (a,b) -> a.startsWith(b)
四种形态:类名::静态方法、对象::实例方法、类名::实例方法(首参当接收者)、类名::new。判断能不能简写的口诀:Lambda 体只干"把这个参数交给某个现成方法"这一件事,就能写 ::。
五、Lambda vs 匿名内部类:同与不同(面试高频)
| 维度 | 匿名内部类 | Lambda |
|---|---|---|
| 语法 | 完整类结构,样板多 | 极简 |
| this 含义 | 指向匿名类实例 | 指向外围实例(没有自己的 this) |
| 类型 | 编译期生成 外部类$1.class |
编译期是普通方法 + invokedynamic 指令,运行期才由 JVM 生成实现类 |
| 覆盖范围 | 抽象类、多抽象方法接口都行 | 只能函数式接口 |
invokedynamic 的收益:Lambda 的实现策略(内联、生成类、MethodHandle)推迟到运行期由 JVM 决定,字节码体积小、未来可优化——这是"Lambda 比匿名内部类轻"的底层依据。
六、实战:回调与策略的全部换血
回调(线程创建就是现成例子):
new Thread(() -> System.out.println("run in " + Thread.currentThread().getName())).start();
策略模式瘦身:枚举/Map 持有 Lambda 代替一堆策略类——
Map<String, Comparator<Integer>> strategies = Map.of(
"asc", Integer::compareTo,
"desc", (a, b) -> b.compareTo(a)
);
list.sort(strategies.get("desc"));
自己写"接受行为"的 API:把 Supplier 当懒加载句柄、Function 当转换管道、Predicate 当校验规则组合——这是现代框架源码(Spring 的 Function 注入、MyBatis-Plus 的 Lambda 查询)的通用语言。
七、高频面试题速答
- Lambda 和匿名内部类区别? 见第五节表格——this 指向、实现机制(invokedynamic)、只限函数式接口。
- 为什么捕获的局部变量要 final? 捕获的是值副本,可变会引发副本与原变量状态不一致;成员变量不受限(通过 this 引用,改的是对象字段)。
- @FunctionalInterface 必须加吗? 不必须(判定标准是"单抽象方法"),加了的收益是编译器校验 + 文档自明。
- 函数式接口里能有 default 方法吗? 能,default/static 不算抽象方法。
- 方法引用什么时候用不了? 需要对参数做前置加工、多语句、或方法调用不"唯一对应参数"时——老老实实写 Lambda。
勾连
- 内部类与变量捕获的同源规则:02-内部类
- effective final 与 final 语义:01-final关键字
- 函数式接口的最大用户:08-Stream与Optional
- 接口 default 方法的冲突裁决:02-抽象类与接口
⬅️ 06-枚举 🏠 00-Java ➡️ 08-Stream与Optional
💬 评论