函数式接口与 Lambda 表达式
本教程共 100 篇 · 第 47 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
47. 函数式接口与 Lambda 表达式
本节目标:搞懂什么是「函数式接口」,学会用
(参数) -> 表达式的 Lambda 语法写匿名行为,摆脱又厚又啰嗦的匿名内部类。
一个让人头大的写法
假设我们要让一段代码「在另一个线程里运行」。以前得这么写:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("干活了");
}
};
```bash
为了表达「运行时要执行什么」这一个动作,却套了 `new Runnable() { ... }`、`@Override`、`public void run` 一大堆样板代码。真正有用的只有中间那一行打印。
Java 8 的 **Lambda 表达式**就是来消灭这种啰嗦的。
## 函数式接口:Lambda 的「类型」
Lambda 不是凭空出现的,它必须「贴」在一个特定接口上。这个接口要满足一个条件:**只有一个抽象方法**。满足这个条件的接口叫**函数式接口**。
`Runnable` 就是典型的函数式接口——它只有一个 `void run()` 抽象方法。正因如此,上面那段代码用 Lambda 可以写成:
```java
Runnable r = () -> System.out.println("干活了");
一个箭头 ->,左边是参数(这里 run() 没有参数,写空括号 ()),右边是要执行的代码。那一堆样板全没了。
Lambda 的基本语法
完整形态是:(参数列表) -> { 语句; },但很多情况下能再简化。
1. 没有参数:
() -> System.out.println("hi")
```bash
**2. 一个参数(括号可省):**
```java
s -> System.out.println(s)
// 等价于 (s) -> System.out.println(s)
3. 多个参数:
(a, b) -> a + b
```java
**4. 多行语句(用大括号 + return):**
```java
(a, b) -> {
int sum = a + b;
return sum;
}
Note如果箭头右边只有「一个表达式」,Java 会自动把它当作返回值,不用写
return和大括号。一旦用了{ },就必须自己写return。
类型推断:参数为什么不用写类型
你可能疑惑:Lambda 里 a、b 没写 int,编译器怎么知道类型?
答案是上下文推断。因为等号右边要赋给一个函数式接口(比如 Comparator<Integer>),编译器从接口的抽象方法签名里反推出参数该是什么类型,所以你不必重复声明。
Comparator<Integer> cmp = (a, b) -> a - b;
```bash
这里 `a`、`b` 被推断为 `Integer`。如果脱离上下文、编译器无法推断,Lambda 就得补全类型,否则报错。
## @FunctionalInterface 注解
你可以给「只有一个抽象方法」的接口加上 `@FunctionalInterface` 注解:
```java
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
它的作用不是「让接口变成函数式接口」——只要只有一个抽象方法,它就是了。这个注解是给编译器看的:强制检查这个接口确实只含一个抽象方法,哪天你手滑加了第二个抽象方法,编译器立刻报错。
Tip自己定义函数式接口时,建议都加上
@FunctionalInterface。它相当于一份「契约声明」,既防误操作,也告诉读代码的人:「这个接口就是给 Lambda 用的」。
用自定义函数式接口写 Lambda
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
public class Main {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
Calculator mul = (a, b) -> a * b;
System.out.println(add.calc(3, 4)); // 7
System.out.println(mul.calc(3, 4)); // 12
}
}
```bash
这里 `add` 和 `mul` 都是 `Calculator` 接口的实例,但我们没写 `new`,没写匿名类,只用 Lambda 把「具体算法」塞了进去。函数式接口把「算法」当成了可以传递的值——这就是函数式编程味道的来源。
## 完整可运行示例(Runnable + 自定义)
```java
@FunctionalInterface
interface Greeter {
void greet(String name);
}
public class Main {
public static void main(String[] args) {
Greeter g = name -> System.out.println("你好," + name);
g.greet("小明");
g.greet("小红");
}
}
运行输出:
你好,小明
你好,小红
```bash
## Lambda 能替掉的经典场景
除了 `Runnable`,Java 标准库里大量接口都是函数式接口,配合 Lambda 立刻变清爽:
- `Comparator<T>`:排序比较器,`(a, b) -> ...` 直接写比较逻辑。
- `Runnable`:线程要执行的任务。
- `Callable<T>`:带返回值的任务。
- `FileFilter`、`Predicate<T>`:条件判断,`x -> x > 0`。
> [!WARNING]
> Lambda 内部访问外部变量时,那个变量必须是**实质 final(effectively final)**的——也就是声明后没被重新赋值。你不能在一个会变的局部变量上让 Lambda 去读写它,否则编译报错。这点和 `final` 的规矩一致。
## 为什么非得是「函数式接口」
你可能会想:Lambda 明明就是一段代码,为什么一定得「贴」在某个接口上?因为 Java 是静态类型语言,任何表达式都得有类型。Lambda 自己没有独立类型,编译器需要靠「目标接口」来推断它长什么样(几个参数、什么返回)。函数式接口就提供了这个「类型锚点」。
标准库已经把常用的函数式接口都备齐了,全在 `java.util.function` 包里,拿来直接用就行。
## 四大内置函数式接口速查
这个包里有几十个接口,但天天要用的就四个。把这张表记牢,后面学 Stream 会顺很多:
| 接口 | 抽象方法 | 干什么 | Lambda 示例 |
|------|----------|--------|-------------|
| `Function<T,R>` | `R apply(T t)` | 收一个值,变成另一个值 | `s -> s.length()` |
| `Consumer<T>` | `void accept(T t)` | 收一个值,只消费不返回 | `s -> System.out.println(s)` |
| `Supplier<T>` | `T get()` | 不收参数,凭空产出一个值 | `() -> "hello"` |
| `Predicate<T>` | `boolean test(T t)` | 收一个值,返回真假 | `n -> n > 0` |
口诀好记:有进有出是 `Function`,只进不出是 `Consumer`,只出不进是 `Supplier`,出的是布尔值就是 `Predicate`。
```java
Function<String, Integer> len = s -> s.length();
Consumer<String> show = s -> System.out.println(s);
Supplier<String> gen = () -> "凭空造一个字符串";
Predicate<String> empty = s -> s.isEmpty();
System.out.println(len.apply("hello")); // 5
show.accept(gen.get()); // 凭空造一个字符串
System.out.println(empty.test("")); // true
Tip写工具方法时,优先用这些标准函数式接口当参数,而不是自己重复定义一堆
@FunctionalInterface,这样别人读起来更顺,也更通用。
方法引用:Lambda 的再简写
如果 Lambda 体里只是「原样调用一个已有方法」,可以用双冒号 :: 再简写一层。这叫方法引用。
// Lambda 写法
Consumer<String> c1 = s -> System.out.println(s);
// 方法引用写法,两者完全等价
Consumer<String> c2 = System.out::println;
```bash
方法引用一共四种形式:
| 形式 | 语法 | 例子 |
|------|------|------|
| 静态方法 | `类名::静态方法` | `Integer::parseInt` |
| 特定对象的实例方法 | `对象::实例方法` | `System.out::println` |
| 任意对象的实例方法 | `类名::实例方法` | `String::length` |
| 构造方法 | `类名::new` | `ArrayList::new` |
第三种最容易绕晕:`String::length` 等价于 `s -> s.length()`,Lambda 的第一个参数被当成了方法的调用者。
> [!TIP]
> 别为了炫技强行用方法引用。只有当 Lambda 体恰好是「一个方法调用,不多不少」时才换成 `::`;一旦要加参数、加判断,老老实实写 Lambda 更好读。
## 小结
- 函数式接口 = 只有一个抽象方法的接口;Lambda 表达式必须贴在函数式接口上使用。
- Lambda 语法:`(参数) -> 表达式/语句块`,能通过上下文自动推断参数类型。
- `@FunctionalInterface` 让编译器帮你守住「只有一个抽象方法」的约束。
- Lambda 把「行为」变成可传递的值,消灭了大量匿名内部类样板代码。
- Lambda 内只能访问实质 final 的外部局部变量。
- 四大内置接口:`Function`(有进有出)、`Consumer`(只进不出)、`Supplier`(只出不进)、`Predicate`(出布尔值)。
- Lambda 体只是一次方法调用时,可以用 `::` 简写成方法引用,共四种形式。
Lambda 是后续集合 Stream 流式处理、以及很多现代 Java 写法的基石。下一章我们看密封类,它是另一种「约束继承关系」的现代特性。