JDK JDK 学习手册8 → 27 · JDK 专用学习站
QUICKREF · LANGUAGE · LAMBDA

Lambda 与函数式接口

JDK 8 · 速查主题 01 / 06 · 预计 30 分钟
核心30 分钟JEP 126
一句话Lambda 把「一段行为」变成可以传递的值,类型由函数式接口决定;方法引用是它的简写。理解目标类型、变量捕获和 this,才算真正会用。

学完你要能自己回答

  1. Lambda 的类型从哪里来?为什么单独写 s -> s.length() 编译不过?
  2. 为什么 Lambda 捕获的局部变量必须是 final 或 effectively final?
  3. 方法引用有哪几种形式?重载时编译器按什么选择?

函数式接口: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::parseInts -> Integer.parseInt(s)
绑定实例System.out::printlns -> System.out.println(s)
未绑定实例String::lengths -> 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。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。