QUICKREF · LANGUAGE · LAMBDA
Lambda 与函数式接口
JDK 8 · 速查主题 01 / 06 · 预计 30 分钟
一句话Lambda 把「一段行为」变成可以传递的值,类型由函数式接口决定;方法引用是它的简写。理解目标类型、变量捕获和 this,才算真正会用。
学完你要能自己回答
- Lambda 的类型从哪里来?为什么单独写
s -> s.length()编译不过? - 为什么 Lambda 捕获的局部变量必须是 final 或 effectively final?
- 方法引用有哪几种形式?重载时编译器按什么选择?
函数式接口:Lambda 的类型基础01
函数式接口是只有一个抽象方法的接口(SAM)。Lambda 本身没有类型,它必须被赋给一个函数式接口,或作为参数传给期望该接口的方法;这个期望类型叫目标类型。没有目标类型,编译器无法知道 s -> s.length() 该实现哪个方法。
| 接口 | 抽象方法 | 典型用途 |
|---|---|---|
Function<T,R> | R apply(T t) | 转换、映射 |
Consumer<T> | void accept(T t) | 消费、打印、写库 |
Supplier<T> | T get() | 延迟提供、工厂 |
Predicate<T> | boolean test(T t) | 过滤、判断 |
UnaryOperator<T> | T apply(T t) | 同类型转换 |
BiFunction<T,U,R> | R apply(T t, U u) | 双参数转换 |
@FunctionalInterface
interface Rule {
boolean test(String s);
}
Rule longWord = s -> s.length() > 3;
System.out.println(longWord.test("lambda"));
true
@FunctionalInterface 只做编译检查:多一个抽象方法就会报错,它不改变接口语义,也不是运行时注解。
默认方法和静态方法不算抽象方法;
Object 的 public 方法(如 toString)也不计入 SAM。因此 Comparator 虽然有很多 default 方法,仍然是函数式接口。写法地图与方法引用02
Lambda 的语法只有「参数列表 → 表达式或代码块」。参数类型可以省略;只有一个参数时可以省略括号;表达式体自动作为返回值。
s -> s.length():单参数、表达式体。(a, b) -> a + b:多参数、表达式体。() -> { System.out.println("run"); }:无参数、代码块。(String s) -> s.isEmpty():显式参数类型,常用于重载消歧。
方法引用是 Lambda 的简写,但目标类型仍然决定绑定。常见的有四种形式:
| 形式 | 例子 | 等价 Lambda |
|---|---|---|
| 静态方法 | Integer::parseInt | s -> Integer.parseInt(s) |
| 绑定实例 | System.out::println | s -> System.out.println(s) |
| 未绑定实例 | String::length | s -> s.length() |
| 构造器 | ArrayList::new | () -> new ArrayList<>() |
List<String> names = new ArrayList<>(Arrays.asList("Ada", "alan", "Grace"));
names.sort((a, b) -> a.compareToIgnoreCase(b));
System.out.println(names);
Function<String, Integer> length = String::length;
System.out.println(length.apply("lambda"));
Supplier<List<String>> factory = ArrayList::new;
System.out.println(factory.get().isEmpty());
[Ada, alan, Grace] 6 true
names.sort(...) 是 JDK 8 新增的 List.sort(Comparator);Comparator.comparing、thenComparing、reversed 也都是 8 才有的。写 8 的代码时不要顺手用 List.of。变量捕获与 this03
Lambda 捕获的是值,不是变量槽。局部变量在方法返回后栈帧就消失;如果 Lambda 还能修改它,语义就无法自洽。因此编译器要求被捕获的局部变量初始化后不再变化,也就是 effectively final。
注意:捕获引用类型时,捕获的是引用本身;对象内容仍然可以变。Lambda 看到的是同一个对象,而不是深拷贝。
public class ThisDemo {
String name = "outer";
void test() {
Runnable lambda = () -> System.out.println(this.name);
Runnable anon = new Runnable() {
String name = "anon";
public void run() { System.out.println(this.name); }
};
lambda.run();
anon.run();
}
public static void main(String[] args) {
new ThisDemo().test();
}
}
outer anon
Lambda 没有自己的 this,所以 this.name 访问外部实例;匿名内部类有自己的 this,访问的是匿名类自己的字段。这是两者最容易混淆的语义差异。
在 Lambda 里修改外部局部变量会编译失败:
local variables referenced from a lambda expression must be final or effectively final。需要可变状态时,优先改设计,而不是用单元素数组或 AtomicInteger 绕过。重载、异常与调试边界04
方法引用和 Lambda 都在编译期绑定到目标类型。同名方法有多个重载时,编译器用目标类型的参数和返回值来选;选不出来就报 ambiguous,需要显式写参数类型或改用代码块 Lambda。
Function<Object, String> f = String::valueOf;
System.out.println(f.apply(42));
42
- 受检异常不能在 Lambda 里直接抛出,除非函数式接口的抽象方法声明了它;否则要在体内 try/catch,或包装成运行时异常并保留 cause。
- Lambda 的栈帧通常显示为合成方法
lambda$...,不是匿名类构造器;调试时不要按类名找源码。 - 不要用 Lambda 给函数式接口实现塞可变状态;那会把无状态函数变成有状态对象,并行时尤其危险。
this、getClass()、序列化行为都与匿名类不同,不要依赖 Lambda 的运行时类名。
重载消歧最可靠的方式是显式写参数类型,例如
(String s) -> ...;不要依赖 IDE 或运行时猜测。版本边界BOUNDARY
这张表比 API 本身更值得记。写代码时先确认你的运行时版本。
| 特性 / API | 状态 | 说明 |
|---|---|---|
| Lambda 表达式 | 8 正式 | JEP 126;JSR 335。编译为 invokedynamic。 |
| 函数式接口 `java.util.function` | 8 新增 | 提供 Function/Consumer/Supplier/Predicate 及基本类型特化,减少装箱。 |
| default / static 接口方法 | 8 正式 | 让 Collection 能新增 stream()/forEach() 而不破坏实现类。 |
| 方法引用 | 8 正式 | 四种形式;编译期绑定目标类型。 |
| `var` 修饰 Lambda 参数 | 11 起 | JDK 8 不能写 (var a, var b) -> ...。 |
| `List.of` / `Map.of` | 9 起 | JDK 8 用 Arrays.asList 或 Collections.unmodifiableList。 |
| `Optional.isEmpty` | 11 起 | JDK 8 只有 !isPresent()。 |
费曼自检SELF CHECK
先合上页面,用自己的话回答,再展开对照。能讲出因果和反例,才算真正掌握。
01. 为什么 Lambda 不能单独存在,必须有一个目标类型?
Lambda 只描述参数和函数体,没有名字,也没有声明它实现哪个接口。编译器需要目标类型来确定抽象方法签名、参数类型和返回类型;没有目标类型就无法生成正确的调用点。
02. Lambda 捕获的局部变量为什么必须 effectively final?
Lambda 捕获的是值快照,不是变量槽。如果允许之后重新赋值,Lambda 内外看到的值会不一致,且栈帧可能已经销毁。effectively final 保证捕获的值在整个 Lambda 生命周期内稳定。
03. Lambda 的 this 和匿名内部类的 this 有什么区别?
Lambda 没有独立的 this,表达式里的 this 指向创建它的外部实例;匿名内部类有自己的实例,this 指向匿名类对象。这也是 Lambda 不能访问自身成员、只能访问外部实例成员的原因。
04. 方法引用和 Lambda 是什么关系?重载时看什么?
方法引用是 Lambda 的简写,二者都绑定到函数式接口的目标类型。重载解析时编译器看目标类型的参数和返回值,选择匹配的方法;无法唯一确定就报 ambiguous,需要显式参数类型或改写 Lambda。
学习进度
记录保存在本机浏览器 · 不上传