Lambda 表达式与方法引用从 Java 8 开始提供,用来为函数式接口创建简洁的实现。它们广泛应用于集合排序、Stream API、异步任务和事件处理。

Lambda 不是脱离类型独立存在的“函数值”。编译器必须从赋值、参数、返回值或强制类型转换等上下文中推断出它的目标函数式接口。

一、函数式接口是基础

函数式接口在概念上只有一个抽象方法。它可以包含 default、static 方法,也可以包含与 Object 公共方法签名相同的方法。@FunctionalInterface 不是必需的,但推荐使用,让编译器帮助维护这一约束。

@FunctionalInterface
interface Calculator {
    int apply(int a, int b);

    default int twice(int value) {
        return apply(value, value);
    }
}

下面两个 Lambda 的形状相同,但目标类型不同,因此具有不同含义:

Runnable task = () -> System.out.println("done");
Callable<String> loader = () -> "done";

二、Lambda 基本语法

(参数列表) -> 表达式
(参数列表) -> {
    语句;
}

常见简化规则

  • 参数类型通常可以省略,由目标类型推断。
  • 只有一个未显式声明类型的参数时,可以省略圆括号。
  • 方法体只有一个表达式时,可以省略大括号和 return。
  • 多条语句必须放入大括号;需要返回值时应显式 return。
Predicate<String> nonEmpty = s -> !s.isEmpty();
Function<String, Integer> length = String::length;
BinaryOperator<Integer> add = (a, b) -> a + b;

Function<Integer, Integer> abs = value -> {
    if (value >= 0) {
        return value;
    }
    return -value;
};

三、常用函数式接口

  • Supplier<T>:无参数,返回 T;核心方法 get()。
  • Consumer<T>:接收 T,不返回结果;核心方法 accept(T)。
  • Function<T, R>:把 T 转换为 R;核心方法 apply(T)。
  • Predicate<T>:接收 T,返回 boolean;核心方法 test(T)。
  • UnaryOperator<T>:接收并返回同一种类型 T。
  • BiFunction<T, U, R>:接收两个参数并返回 R。
  • BinaryOperator<T>:接收两个 T,并返回 T。
  • Runnable、Callable<V>、Comparator<T> 也都是常见函数式接口。
处理大量基本类型数据时,可以使用 IntFunction、IntPredicate、ToIntFunction 等特化接口,减少不必要的装箱和拆箱。

四、典型使用场景

简化 Runnable

// 匿名内部类
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
}).start();

// Lambda
new Thread(() -> System.out.println("Hello")).start();

集合排序

List<String> languages = new ArrayList<>(
    List.of("java", "python", "go")
);

languages.sort(
    Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder())
);

Stream API

List<String> result = languages.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList();

result.forEach(System.out::println);

自定义函数式接口

Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;

System.out.println(add.apply(1, 2));       // 3
System.out.println(multiply.apply(3, 4));  // 12

五、什么是方法引用

当 Lambda 的主要工作只是调用一个已经存在的方法时,可以使用 :: 写成方法引用。方法引用仍然需要目标函数式接口,编译器会根据接口方法的参数和返回类型完成适配。

Consumer<String> printer = value -> System.out.println(value);
Consumer<String> samePrinter = System.out::println;

六、四类方法引用

1. 静态方法:ClassName::staticMethod

Function<String, Integer> parser = Integer::parseInt;
Integer value = parser.apply("42");

2. 特定对象的实例方法:object::instanceMethod

PrintStream output = System.out;
Consumer<String> printer = output::println;

List<String> names = List.of("Tom", "Jack");
names.forEach(output::println);

3. 某类型任意对象的实例方法:TypeName::instanceMethod

这种形式会把函数式接口的第一个参数作为方法接收者,其余参数作为方法参数。

BiFunction<String, String, Integer> comparator =
    String::compareToIgnoreCase;

// 等价于:
BiFunction<String, String, Integer> sameComparator =
    (left, right) -> left.compareToIgnoreCase(right);

names.stream()
    .sorted(String::compareToIgnoreCase)
    .forEach(System.out::println);

4. 构造器引用:ClassName::new

Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();

Function<String, User> userFactory = User::new;
User user = userFactory.apply("Tom");

七、变量捕获与作用域

Lambda 可以读取外层局部变量,但该变量必须是 final 或 effectively final,即赋值后不再发生变化。实例字段和静态字段不受这一局部变量限制。

int minLength = 4;

Predicate<String> longEnough =
    value -> value.length() >= minLength;

// minLength = 5; // 如果重新赋值,上面的 Lambda 将无法编译
  • Lambda 不创建新的 this 语义;其中的 this 指向外层实例。
  • Lambda 参数不能与外层局部变量重名。
  • 并发执行 Lambda 时,捕获对象的可变状态仍需考虑线程安全。

八、常见易错点

  • 不要用 a.length() - b.length() 实现通用比较,极端数值比较可能溢出;优先使用 Comparator.comparingInt。
  • 方法引用只有在意图更清晰时才值得使用;复杂转换继续使用 Lambda。
  • 重载方法可能导致目标类型歧义,可通过显式变量类型或强制转换解决。
  • 标准 Function、Consumer 等接口不会自动处理受检异常,需要捕获、包装或定义允许抛出异常的接口。
  • 避免在 Stream 的中间操作中依赖副作用,可读性和并行执行都更可靠。

九、快速记忆

Lambda:
(参数) -> 表达式 / 语句块

方法引用:
ClassName::staticMethod
object::instanceMethod
TypeName::instanceMethod
ClassName::new
先确认目标函数式接口,再判断 Lambda 是否只是调用现有方法;如果是,并且参数能够自然对应,就可以考虑改成方法引用。