首页 / Java 入门教程 / 函数式接口与 Lambda 表达式

Java 入门教程

函数式接口与 Lambda 表达式

本教程共 100 篇 · 第 47 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程Lambda函数式接口FunctionalInterfaceJava8

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 里 ab 没写 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 写法的基石。下一章我们看密封类,它是另一种「约束继承关系」的现代特性。