Lambda 与方法引用 :把代码写成「一句话」
Java Lambda 与方法引用 :::把代码写成「一句话」
先看一个「痛苦」的时刻。
假设你有个需求:从一个列表里,过滤出「偶数」。在 Java 8 之前,你要写一个匿名内部类:
List<Integer> evens = new ArrayList<>(); for (Integer n : numbers) { if (n % 2 == 0) { evens.add(n); } }或者,如果你用的是
Comparator、Runnable这种「只有一个方法的接口」,匿名内部类能写到你怀疑人生:// 一个简单的排序,居然要写 7 行样板代码 list.sort(new Comparator<User>() { @Override public int compare(User a, User b) { return a.getAge() - b.getAge(); } });你心里真正想表达的,其实只有一句:「按年龄比大小」。但为了这一句,你得把「new 一个类」「重写方法」「传两个参数」「返回结果」这些废话全写一遍。
而有了 Lambda 表达式,同样的事,一句话就够:
list.sort((a, b) -> a.getAge() - b.getAge());这,就是 Lambda 的使命——把「传递行为」这件事,从七八行样板代码,压缩成一句话。
今天这篇,面向 Java 学习者,把 Lambda 和方法引用
::这两个语法,从「怎么写」到「底层原理」,彻底讲透。老规矩,先上图,再讲理。
一、什么是 Lambda?为什么需要它?
1.1 从「传数据」到「传行为」
在传统 Java 里,方法的参数都是「数据」——传个 int、传个 String、传个对象。但很多时候,我们想传的不是「一个值」,而是「一段逻辑 / 一个行为」。
比如「排序」,你想传的是「怎么比大小」这个行为,而不是某个具体的数字。
1.2 Lambda 的本质:一个「可以传递的代码块」
Lambda 表达式的本质,就是「一个可以被当作参数传来传去的代码块」。 它让你能把一段逻辑,像变量一样赋值、传递、调用。
1.3 Lambda 的语法拆解
Lambda 的语法,一眼就能看懂:
(参数列表) -> { 方法体 }
| 部分 | 含义 | 举例 |
|---|---|---|
(参数列表) | 这段逻辑接收什么输入 | (a, b) |
-> | 箭头(读作「变成」) | 分隔符 |
{ 方法体 } | 这段逻辑干什么 | { return a - b; } |
// 无参数
() -> System.out.println("Hello");
// 一个参数(括号可省略)
x -> x * 2;
// 多个参数
(a, b) -> a + b;
// 多行方法体(用花括号 + return)
(a, b) -> {
int result = a + b;
return result;
};
一句话:Lambda 就是「一个能塞进变量里、能当参数传的代码块」。它把「传一个行为」这件事,变得像「传一个数字」一样简单。
二、函数式接口:Lambda 的「宿主」(原理重点)
你可能会问:Lambda 这么「轻飘飘」的一段代码,它到底是个什么类型? 答案是——它必须「装进」一个函数式接口里。
2.1 什么是函数式接口?
函数式接口(Functional Interface),就是「只有一个抽象方法的接口」。
@FunctionalInterface // 这个注解用来标记「我是函数式接口」
public interface Comparator<T> {
int compare(T o1, T o2); // 只有一个抽象方法
}
只要一个接口有且只有一个抽象方法,它就能接收 lambda。
2.2 为什么 lambda 只能装进函数式接口?
因为 lambda 只描述「一个行为」。如果接口里有两个抽象方法,编译器就不知道这段 lambda 到底对应哪个方法了。
2.3 @FunctionalInterface 注解
这个注解不是必需的,但建议加上——它的作用是让编译器帮你检查:万一你不小心往接口里多加了一个方法,编译器会立刻报错。
一句话:Lambda 没有「自己的类型」,它必须依附于一个函数式接口。你写 lambda,本质上是在「用一句话,实现那个唯一的抽象方法」。
三、四大内置函数式接口(实操重点)
每次都要自己定义函数式接口,太麻烦了。Java 8 贴心地内置了四个最常用的,覆盖了 90% 的场景。
3.1 Function<T, R>:输入 → 输出
接收一个参数,返回一个结果。就像一台「加工机器」。
Function<String, Integer> lengthFunc = s -> s.length();
System.out.println(lengthFunc.apply("hello")); // 5
| 方法 | 作用 |
|---|---|
R apply(T t) | 执行转换 |
andThen / compose | 组合多个 Function |
3.2 Predicate:判断(返回 boolean)
接收一个参数,返回 true / false。用来做「过滤、判断」。
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(3)); // false
3.3 Consumer:消费(不返回)
接收一个参数,但不返回任何东西。用来「干一件事」(打印、存库、发消息)。
Consumer<String> printer = s -> System.out.println(s);
printer.accept("Hello Lambda"); // 打印 Hello Lambda
3.4 Supplier:提供(无输入,有输出)
不接收参数,只返回一个结果。相当于「供货商」,你要它就给。
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // 0.1234567...
3.5 四大接口汇总表
| 接口 | 形状 | 作用 | 常用方法 | 典型场景 |
|---|---|---|---|---|
Function<T,R> | 有进有出 | 转换 | apply | map、类型转换 |
Predicate<T> | 有进,出 boolean | 判断 | test | filter |
Consumer<T> | 有进,无出 | 消费 | accept | forEach |
Supplier<T> | 无进,有出 | 提供 | get | 生成值 |
一句话:这四大接口对应四种「形状」——Function 转换、Predicate 判断、Consumer 消费、Supplier 提供。看到 lambda,先判断它是哪种「形状」,就不容易晕。
四、Lambda 的常见写法(实操重点)
Lambda 有很多「简写」规则,第一次看容易懵,这里一次讲清。
4.1 无参、单参、多参
// 无参:必须写空括号
Runnable r = () -> System.out.println("run");
// 单参:括号可以省略
Consumer<String> c = s -> System.out.println(s);
// 等价于
Consumer<String> c2 = (s) -> System.out.println(s);
// 多参:必须写括号
BinaryOperator<Integer> add = (a, b) -> a + b;
4.2 单行表达式 vs 多行语句块
// 单行:不用写 return,直接写表达式(结果自动返回)
Function<Integer, Integer> doubleIt = x -> x * 2;
// 多行:要写花括号 + 显式 return
Function<Integer, Integer> doubleIt2 = x -> {
int result = x * 2;
return result;
};
关键区别:单行是「表达式」,自动返回;多行是「语句块」,必须显式 return。
4.3 参数类型可以省略
// 可以写类型
BinaryOperator<Integer> add1 = (Integer a, Integer b) -> a + b;
// 通常省略(编译器能推断)
BinaryOperator<Integer> add2 = (a, b) -> a + b;
4.4 简写规则汇总表
| 规则 | 说明 | 例子 |
|---|---|---|
| 参数类型可省 | 编译器自动推断 | (a, b) -> ... |
| 单参数括号可省 | 只有一个参数时 | x -> x * 2 |
| 单行可省 return | 方法体只有一句表达式 | x -> x * 2 |
| 多行必须 return | 方法体是多行语句块 | x -> { ...; return x; } |
一句话:Lambda 的简写原则是「能省就省」——类型能推断就省、单参数括号能省就省、单行表达式连 return 都能省。省到极致,就是最优雅的一行。
五、方法引用 ::(实操重点)
方法引用(::)是 Lambda 的「更简写法」。当你发现「这个 lambda 只是调用一个已经存在的方法」时,就可以用 :: 再简化一步。
5.1 核心思想
// 用 lambda
list.forEach(s -> System.out.println(s));
// 用方法引用(更简洁)
list.forEach(System.out::println);
方法引用就是「lambda 的缩写」——把「输入参数原封不动地传给某个方法」这种场景,压缩成一个 ::。
5.2 四种形式逐一讲
① 静态方法引用:类名::静态方法
// lambda 写法
Function<String, Integer> f1 = s -> Integer.parseInt(s);
// 方法引用写法
Function<String, Integer> f2 = Integer::parseInt;
② 实例方法引用(特定对象):对象::实例方法
// 假设有个对象 obj
String prefix = "Hello ";
// lambda 写法
Function<String, String> f1 = s -> prefix.concat(s);
// 方法引用写法
Function<String, String> f2 = prefix::concat;
③ 实例方法引用(类名):类名::实例方法
这种最绕,要重点看。它和「静态方法引用」长得一样,但含义不同——第一个参数,会作为「方法调用的对象」。
// lambda 写法:参数 s 是对象,调用它的 length() 方法
Function<String, Integer> f1 = s -> s.length();
// 方法引用写法:String::length
Function<String, Integer> f2 = String::length;
④ 构造器引用:类名::new
// lambda 写法
Supplier<User> s1 = () -> new User();
// 方法引用写法
Supplier<User> s2 = User::new;
// 带参数的构造器(用 Function)
Function<String, User> f = User::new; // 对应 User(String name) 构造器
5.3 四种形式汇总表
| 形式 | 写法 | 例子 | 对应 lambda |
|---|---|---|---|
| 静态方法引用 | 类名::静态方法 | Integer::parseInt | s -> Integer.parseInt(s) |
| 实例方法引用(对象) | 对象::实例方法 | prefix::concat | s -> prefix.concat(s) |
| 实例方法引用(类名) | 类名::实例方法 | String::length | s -> s.length() |
| 构造器引用 | 类名::new | User::new | () -> new User() |
一句话:方法引用就是 lambda 的「偷懒版」——当 lambda 只是「把参数原样传给某个方法」,就用
::代替。其中类名::实例方法最绕,记住「第一个参数是调用对象」就行。
六、Lambda 与匿名内部类的区别(原理重点)
Lambda 看起来和匿名内部类很像,但它们是本质不同的两样东西。
| 维度 | 匿名内部类 | Lambda |
|---|---|---|
| 本质 | 真的生成一个类 | 不生成类(动态指令实现) |
| this 含义 | 指向匿名内部类自己 | 指向外部类的 this |
| 抽象方法数量 | 可实现多个方法 | 只能对应一个方法 |
| 代码量 | 冗长 | 简洁 |
| 能否有状态 | 可以有字段、状态 | 无状态,只是行为 |
6.1 最关键的区别:this 的含义
这是面试爱考的坑:
public class Test {
public void test() {
// 匿名内部类:this 指向这个匿名类自己
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // 打印的是「匿名内部类对象」
}
};
// lambda:this 指向「外部类 Test」的实例
Runnable r2 = () -> System.out.println(this); // 打印的是 Test 对象
}
}
一句话:Lambda 没有自己的
this,它的this直接就是「外面的那个 this」。这也是为什么 lambda 更「轻」——它压根不是一个独立的类。
七、变量捕获与闭包(原理重点)
Lambda 有个著名的「坑」:它可以用外部的变量,但不能修改它们。
7.1 可以捕获,但必须是 effectively final
int num = 10; // 这个变量没有被修改过,是「effectively final」
Runnable r = () -> System.out.println(num); // ✅ 可以捕获、可以读
num = 20; // ❌ 如果后面改了它,上面那段 lambda 就会编译报错
所谓 effectively final(事实上的 final):不要求你写 final 关键字,但要求这个变量从头到尾只赋值一次。
7.2 为什么不能改?
int sum = 0;
Runnable r = () -> sum = 100; // ❌ 编译报错!lambda 不能修改外部局部变量
原因是 lambda 可能会在另一个线程里执行,如果允许随意改外部变量,就会引发线程安全问题。Java 干脆「一刀切」:局部变量在 lambda 里只能读,不能改。
7.3 局部变量 vs 成员变量
| 变量类型 | 能否被 lambda 修改 | 原因 |
|---|---|---|
| 局部变量 | ❌ 不能改,只能读 | 线程安全考虑,要求 effectively final |
| 成员变量 / 静态变量 | ✅ 可以改 | 存在堆上,能通过引用访问 |
public class Test {
int count = 0; // 成员变量,可以改
public void test() {
Runnable r = () -> count++; // ✅ 成员变量可以改
int local = 0;
Runnable r2 = () -> local++; // ❌ 局部变量不能改
}
}
一句话:Lambda 能「借用」外面的局部变量(只读),但不能「篡改」它们。想跨 lambda 共享可变状态?用成员变量,或者干脆别用可变状态。
八、Lambda 的底层原理与性能(原理重点)
很多人以为「lambda 就是匿名内部类的语法糖」。大错特错,它们底层的实现完全不同。
8.1 不是语法糖:不生成 class 文件
匿名内部类编译后,会真的生成一个 .class 文件(比如 Test$1.class),还会在运行时 new 出对象。而 lambda 不会生成额外的类文件。
8.2 invokedynamic:延迟到运行时才决定
Lambda 编译后,会变成一个 invokedynamic 指令。这个指令的意思是:「先别急着生成实现,等真正用到的时候,再动态地创建一个实现」。
这样做的好处:
| 好处 | 说明 |
|---|---|
| 不生成类文件 | 减少类的数量,启动更快 |
| 延迟绑定 | 运行时可优化、可缓存 |
| 性能更好 | 首次调用后,实现会被 JVM 缓存复用 |
8.3 性能对比
| 维度 | 匿名内部类 | Lambda |
|---|---|---|
| 编译产物 | 生成 class 文件 | 不生成 |
| 内存占用 | 每次 new 一个对象 | 首次调用后缓存复用 |
| 启动速度 | 类的加载开销大 | 更轻量 |
一句话:Lambda 不是匿名内部类的语法糖,它是靠
invokedynamic动态实现的「轻量级行为」——不生成类、不额外 new 对象,所以更高效。这也是它能和 Stream 一起,让代码又简洁又快的底层原因。
最后送你一句话:
Lambda 学的不只是语法,而是一种「把行为当数据」的思维。 当你习惯了「传一段逻辑」和「传一个数字」一样自然,你会发现,那些曾经又臭又长的样板代码,都变成了能一眼看懂的、优雅的一句话。
更多推荐

所有评论(0)