Skip to content

Lambda 表达式

概述

Lambda 表达式(Lambda Expression)是 JDK 8 引入的语法特性,用于简洁地表示可传递的匿名函数。它允许把一段代码(行为)当作参数传递,从而替代冗长的匿名内部类,让代码更简洁、可读性更好,并为 Stream API 和函数式编程风格打下基础。

通俗理解:Lambda 就是「匿名函数」的简写——不写方法名、不写 return 和花括号(在简单形式下),只保留「参数 + 箭头 + 表达式」,让「做什么」一目了然。

前置建议

已掌握 接口内部类 的基本概念,了解匿名内部类的写法。理解 泛型 有助于阅读带泛型的函数式接口。


为什么需要 Lambda

在 JDK 8 之前,若要传递「一段逻辑」,通常需要写匿名内部类,代码冗长且把真正的逻辑埋在样板代码里:

java
// 匿名内部类:只关心「打印一句话」,却要写一整套类结构
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, Lambda!");
    }
};
new Thread(r).start();

用 Lambda 改写后,意图更清晰:

java
// Lambda:直接表达「无参、执行一句打印」
Runnable r = () -> System.out.println("Hello, Lambda!");
new Thread(r).start();

Lambda 带来的好处

  • 简洁:减少样板代码,突出核心逻辑。
  • 可读:更接近「行为描述」而非「类型描述」。
  • 与 API 结合:与 Stream APIOptional 以及集合的 forEach 等配合自然。

函数式接口

Lambda 不能单独存在,必须赋值给或传递给一个「函数式接口」类型的变量或形参

函数式接口(Functional Interface)是指:只包含一个抽象方法的接口(可以有多个默认方法或静态方法)。JDK 8 用 @FunctionalInterface 标注,编译器会检查是否真的只有一个抽象方法。

常见函数式接口示例:

接口抽象方法典型用途
Runnablevoid run()无参、无返回值
Supplier<T>T get()无参、返回 T
Consumer<T>void accept(T t)接收一个 T,无返回值
Function<T, R>R apply(T t)接收 T,返回 R
Predicate<T>boolean test(T t)接收 T,返回 boolean
Comparator<T>int compare(T a, T b)比较两个 T

自定义函数式接口示例:

java
@FunctionalInterface
public interface Greeter {
    void say(String message);  // 仅此一个抽象方法
}

// 使用 Lambda 实现
Greeter g = msg -> System.out.println("Hi, " + msg);
g.say("Java");  // 输出: Hi, Java

提示

@FunctionalInterface 不是必须的,但加上后编译器会帮你检查「只有一个抽象方法」,避免误加第二个抽象方法导致 Lambda 无法使用。


Lambda 语法

基本形式

( 参数列表 ) -> 表达式或语句块
  • 参数列表:与函数式接口中抽象方法的参数一致;只有一个参数时可省略括号。
  • 箭头->,读作「变成」「得到」。
  • 右侧
    • 表达式:直接写表达式,其值即为返回值(无需写 return)。
    • 语句块:用 { } 包住多条语句;若有返回值,需写 return

常见写法对比

java
// 无参
Runnable r1 = () -> System.out.println("run");

// 单参可省括号
Consumer<String> c = s -> System.out.println(s);

// 多参必须写括号
Comparator<String> cmp = (a, b) -> a.compareTo(b);

// 表达式体:自动返回结果
Function<String, Integer> len = s -> s.length();

// 语句块:多条语句或需要 return
Function<String, Integer> len2 = s -> {
    if (s == null) return 0;
    return s.length();
};

使用示例

示例 1:Runnable 与线程

java
// 传统匿名内部类
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}).start();

// Lambda
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();

示例 2:Comparator 与排序

java
import java.util.*;

List<String> list = Arrays.asList("banana", "apple", "cherry");

// 按字符串长度排序
list.sort((a, b) -> a.length() - b.length());

// 按字典序
list.sort((a, b) -> a.compareTo(b));

// 等价于 Comparator 的静态方法(方法引用,后续可深入)
list.sort(Comparator.comparing(String::length));

示例 3:集合 forEach 与 Predicate

java
import java.util.*;

List<String> names = Arrays.asList("Tom", "Jerry", "Spike");

// forEach 接受 Consumer<String>
names.forEach(name -> System.out.println("Hello, " + name));

// 过滤:用 Predicate 表示「是否保留」
names.stream()
     .filter(name -> name.length() > 3)
     .forEach(System.out::println);  // 仅 Jerry, Spike

变量捕获(闭包)

Lambda 表达式中可以使用外层作用域的变量,但该变量必须是 effectively final(事实上的 final):即要么用 final 修饰,要么在赋值后不再被修改。

java
int base = 10;
// base 在 Lambda 之后不再被修改,即 effectively final
Function<Integer, Integer> addBase = x -> x + base;
System.out.println(addBase.apply(5));  // 15

注意

在 Lambda 内不能修改捕获的局部变量(例如 base++),否则编译错误。若需要可变状态,应使用数组或对象引用等,或考虑其他设计。

java
// 错误示例:在 Lambda 中修改捕获的变量
int count = 0;
Runnable r = () -> count++;  // 编译错误:Variable used in lambda should be final or effectively final

注意事项

  • 类型可推断:多数情况下编译器能根据目标类型(函数式接口)推断参数类型,无需写 (String s),写 (s)s 即可。
  • 仅用于函数式接口:Lambda 的类型由「目标类型」决定,只能用在需要单个抽象方法接口的地方。
  • 与匿名内部类的区别:Lambda 不是匿名内部类的语法糖在所有层面都等价(如 this 的语义、作用域),但在「实现一个函数式接口」的常见用法下,可以认为 Lambda 是更简洁的写法。

提示

需要「带名字」的复用逻辑时,仍可写普通方法;若该方法的签名与某函数式接口的抽象方法一致,可用方法引用(如 String::lengthSystem.out::println)代替 Lambda,使代码更简洁。详见 Stream API 中的方法引用用法。


相关链接

基于 VitePress 构建