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 和分号

三条语法要点:

  1. 参数类型可省——编译器从目标类型推断(见下节"函数式接口");
  2. 体是单个表达式时可省大括号与 return;多条语句必须 {} 且显式 return;
  3. 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 查询)的通用语言。

七、高频面试题速答

  1. Lambda 和匿名内部类区别? 见第五节表格——this 指向、实现机制(invokedynamic)、只限函数式接口。
  2. 为什么捕获的局部变量要 final? 捕获的是值副本,可变会引发副本与原变量状态不一致;成员变量不受限(通过 this 引用,改的是对象字段)。
  3. @FunctionalInterface 必须加吗? 不必须(判定标准是"单抽象方法"),加了的收益是编译器校验 + 文档自明。
  4. 函数式接口里能有 default 方法吗? 能,default/static 不算抽象方法。
  5. 方法引用什么时候用不了? 需要对参数做前置加工、多语句、或方法调用不"唯一对应参数"时——老老实实写 Lambda。

勾连


⬅️ 06-枚举 🏠 00-Java ➡️ 08-Stream与Optional