函数式接口与 Lambda 表达式
本教程共 100 篇 · 第 47 篇 · 更新于 2026-08-05 · 约 5 分钟阅读
本节目标:搞懂什么是「函数式接口」,学会用
(参数) -> 表达式的 Lambda 语法写匿名行为,摆脱又厚又啰嗦的匿名内部类。
一个让人头大的写法
假设我们要让一段代码「在另一个线程里运行」。以前得这么写:
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("干活了");
}
};
为了表达「运行时要执行什么」这一个动作,却套了 new Runnable() { ... }、@Override、public void run 一大堆样板代码。真正有用的只有中间那一行打印。
Java 8 的 Lambda 表达式就是来消灭这种啰嗦的。
函数式接口:Lambda 的「类型」
Lambda 不是凭空出现的,它必须「贴」在一个特定接口上。这个接口要满足一个条件:只有一个抽象方法。满足这个条件的接口叫函数式接口。
Runnable 就是典型的函数式接口——它只有一个 void run() 抽象方法。正因如此,上面那段代码用 Lambda 可以写成:
Runnable r = () -> System.out.println("干活了");
一个箭头 ->,左边是参数(这里 run() 没有参数,写空括号 ()),右边是要执行的代码。那一堆样板全没了。
Lambda 的基本语法
完整形态是:(参数列表) -> { 语句; },但很多情况下能再简化。
1. 没有参数:
() -> System.out.println("hi")
2. 一个参数(括号可省):
s -> System.out.println(s)
// 等价于 (s) -> System.out.println(s)
3. 多个参数:
(a, b) -> a + b
4. 多行语句(用大括号 + return):
(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;
这里 a、b 被推断为 Integer。如果脱离上下文、编译器无法推断,Lambda 就得补全类型,否则报错。
@FunctionalInterface 注解
你可以给「只有一个抽象方法」的接口加上 @FunctionalInterface 注解:
@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
}
}
这里 add 和 mul 都是 Calculator 接口的实例,但我们没写 new,没写匿名类,只用 Lambda 把「具体算法」塞了进去。函数式接口把「算法」当成了可以传递的值——这就是函数式编程味道的来源。
完整可运行示例(Runnable + 自定义)
@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("小红");
}
}
运行输出:
你好,小明
你好,小红
Lambda 能替掉的经典场景
除了 Runnable,Java 标准库里大量接口都是函数式接口,配合 Lambda 立刻变清爽:
Comparator<T>:排序比较器,(a, b) -> ...直接写比较逻辑。Runnable:线程要执行的任务。Callable<T>:带返回值的任务。FileFilter、Predicate<T>:条件判断,x -> x > 0。
WarningLambda 内部访问外部变量时,那个变量必须是**实质 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。
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;
方法引用一共四种形式:
| 形式 | 语法 | 例子 |
|---|---|---|
| 静态方法 | 类名::静态方法 | 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 写法的基石。下一章我们看密封类,它是另一种「约束继承关系」的现代特性。