

🔥个人主页:代码不加冰(欢迎来访) 🎬作者简介:java后端学习者 ❄️个人专栏:LeetCode刷题日记 , 苍穹外卖日记,SSM框架深入,JavaWeb, ✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好,我是代码不加冰,最近在看项目的时候一直被一个东西困扰,那就是Lambda表达式,我是知道这个知识点的,但是看起来就是感觉怪怪的,尤其是Mybatis Plus里面使用的Lambda表达式,再结合最近在八股中刚看到这里,这里给大家来个Lambda表达式大杂烩,彻底搞清。
摘要:
本文全面解析了Java Lambda表达式及其在MyBatisPlus中的应用。
首先介绍了Lambda的基本语法和函数式接口概念,阐述了其底层通过invokedynamic指令实现而非匿名内部类的原理。重点剖析了MyBatisPlus如何利用可序列化Lambda特性,通过反解析方法引用获取数据库字段名的技术实现:自定义Serializable函数式接口,序列化Lambda获取方法元信息,转换为属性名后映射为数据库列名,并采用缓存优化性能。文章还对比了Lambda与传统写法的优劣,指出在类型安全和可维护性方面的优势,同时对常见面试问题进行了归纳解答,为开发者深入理解Lambda及其在ORM框架中的应用提供了系统性的指导。
一、Lambda 表达式是什么
1.1 一句话定义
Lambda 表达式是 Java 8 引入的一种匿名函数,本质上是对函数式接口(Functional Interface)的实例的一种简洁写法。
换句话说:Lambda 表达式不是随便写的语法糖,它必须依附于一个函数式接口才能存在。
1.2 语法结构
(参数列表) -> { 方法体 }
几种常见写法:
// 无参数
() -> System.out.println("Hello Lambda");
// 单个参数(括号可省略)
name -> System.out.println("Hello " + name);
// 多个参数
(a, b) -> a + b;
// 方法体多行,需要用 {} 包裹,并显式 return
(a, b) -> {
int sum = a + b;
return sum;
};
1.3 从匿名内部类到 Lambda 的演变
假设有这样一个函数式接口:
interface Calculator {
int calc(int a, int b);
}
匿名内部类写法(Java 8 之前):
Calculator add = new Calculator() {
@Override
public int calc(int a, int b) {
return a + b;
}
};
Lambda 写法:
Calculator add = (a, b) -> a + b;
一眼看出:Lambda 省去了接口名、方法名、 关键字这些代码,只保留了参数和逻辑本身。
二、函数式接口:Lambda 的基石
2.1 什么是函数式接口
只包含一个抽象方法的接口,称为函数式接口(可以有默认方法、静态方法,但抽象方法只能有一个)。
@FunctionalInterface
public interface Calculator {
int calc(int a, int b);
}
@FunctionalInterface注解不是必须的,但建议加上——编译器会帮你检查这个接口是否真的只有一个抽象方法,防止后续维护时被人不小心加成两个方法导致 Lambda 编译失败。
面试高频问法:“函数式接口能有默认方法吗” —— 能,默认方法和静态方法不算在“抽象方法”的计数里,因为它们有方法体,不需要 Lambda 去实现。
2.2 Java 内置的四大函数式接口
Java 在 包中提供了一批现成的函数式接口,日常开发基本够用:java.util.function
| Supplier<T> | T get() | 无参数,返回一个值 | 供应商,只产出不消费 |
| Consumer<T> | void accept(T t) | 接收一个参数,无返回值 | 消费者,只消费不产出 |
| Function<T, R> | R apply(T t) | 接收 T,返回 R | 类型转换/加工 |
| Predicate<T> | boolean test(T t) | 接收 T,返回布尔值 | 条件判断/过滤 |
示例:
Supplier<String> supplier = () -> "Hello";
Consumer<String> consumer = s -> System.out.println(s);
Function<Integer, String> function = i -> "数字是: " + i;
Predicate<Integer> predicate = i -> i > 0;
还有一些变体,比如 (两个入参)、(入参和返回类型相同)、 等,用法类似,不再展开。
三、方法引用:Lambda 的进一步简化
当 Lambda 表达式的方法体只是调用一个已有方法时,可以用方法引用进一步简写,用 表示。::
四种形式:
// 1. 静态方法引用:类名::静态方法名
Function<String, Integer> f1 = Integer::parseInt;
// 等价于 s -> Integer.parseInt(s)
// 2. 实例方法引用(已有对象):对象名::方法名
String str = "hello";
Supplier<Integer> f2 = str::length;
// 等价于 () -> str.length()
// 3. 实例方法引用(未指定对象,第一个参数作为调用者):类名::方法名
Function<String, Integer> f3 = String::length;
// 等价于 s -> s.length()
// 4. 构造方法引用:类名::new
Supplier<ArrayList<String>> f4 = ArrayList::new;
// 等价于 () -> new ArrayList<>()
这个知识点是理解 MyBatis Plus 的关键前置知识,请务必先看懂再往下读
四、Lambda 的底层原理
这是八股文高频考点,很多人只会用不知道原理,面试很容易被问倒。
4.1 Lambda 编译后是什么
很多人误以为 Lambda 会被编译成匿名内部类,这是错误的
正确答案:Lambda 表达式在编译期会被转换为对 字节码指令的调用,具体的实现逻辑是在运行时通过 动态生成一个实现了目标函数式接口的类(并不是传统意义上的匿名内部类 文件)
流程简述:
为什么这么设计
- 避免了像匿名内部类那样,每写一个 Lambda 就在编译期生成一个 文件,减少类文件数量,加快启动加载速度;
- 真正的类生成推迟到运行时,且只生成一次,兼具性能与灵活性。
一句话总结:Lambda 表达式在字节码层面通过 invokedynamic 指令实现,运行时由 LambdaMetafactory 动态生成实现类,而不是编译期生成匿名内部类。
4.2 Lambda 与匿名内部类的区别
| 编译产物 | 编译期生成独立的 文件.class | 编译期只生成方法+invokedynamic指令,运行时动态生成类 |
| 此指向 | 指向匿名内部类自身 | 指向外部类(不会创建新的作用域) |
| 适用范围 | 任意接口/抽象类 | 只能是函数式接口 |
| 可读性 | 冗长 | 简洁 |
| 性能 | 类加载时即生成 | 首次调用时生成,之后复用,理论上有更好的优化空间 |
4.3 变量捕获:为什么 Lambda 里用的局部变量必须是 effectively final
int num = 10;
Runnable r = () -> System.out.println(num);
// num = 20; // 如果加上这行,编译报错!
原因:Lambda 表达式捕获外部局部变量时,是值拷贝而非引用。如果外部变量后续被修改,Lambda 内部拿到的值和外部真实值就会不一致,为了避免这种“隐性 bug”,Java 强制要求被 Lambda 捕获的局部变量必须是事实最终变量(实际上是 final,即赋值后从未再被改动,即使不加 关键字也一样受此约束)。final
五、MyBatis Plus 中的 Lambda 用法详解
铺垫完 Java 基础,终于进入本文重点:MyBatis Plus(简称 MP)如何巧妙地借助 Lambda 表达式,实现字段名的类型安全引用。
5.1 痛点:传统 Wrapper 写法的问题
MyBatis Plus 最早提供的 是这样写的:QueryWrapper
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", "张三");
问题很明显:字段名是字符串()。这意味着:"user_name"
5.2 解决方案:LambdaQueryWrapper
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, "张三");
这里 就是我们前面讲的方法引用,本质上是一个 类型的对象(MP 自定义的一个可序列化函数式接口)。User::getUserName
好处显而易见:
- 字段写错了,编译不通过,IDE 直接报错;
- 数据库字段改名重构时, 实体类的 getter 方法名一并重构,IDE 能自动帮你找到所有引用点。
常用写法一览:
List<User> list = userMapper.selectList(
new LambdaQueryWrapper<User>()
.eq(User::getUserName, "张三")
.gt(User::getAge, 18)
.like(User::getEmail, "@qq.com")
.orderByDesc(User::getCreateTime)
);
在 Service 层,MP 还提供了链式写法(),更简洁:
List<User> list = userService.lambdaQuery()
.eq(User::getUserName, "张三")
.gt(User::getAge, 18)
.list();
更新操作类似,用 :LambdaUpdateWrapper
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(User::getId, 1L)
.set(User::getUserName, "李四");
userMapper.update(null, updateWrapper);
5.3 核心原理:MP 是怎么从拿到 “user_name” 字段名的
这是 MP 相关的面试高频题,也是本文的核心。
先思考一个问题: 传进去的,只是一个“能调用 getUserName 方法”的函数对象,MP 怎么知道它对应的是哪个字段、数据库列名又是什么
答案:利用 Lambda 表达式的可序列化特性,反序列化拿到方法元信息。
具体步骤:
MP 没有直接用 JDK 自带的 ,而是自定义了一个接口:Function<T, R>
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}
关键在于继承了 。
这是 JDK 的一个隐藏特性:当一个 Lambda 表达式对应的目标类型(函数式接口)继承了 ,那么这个 Lambda 编译后生成的类会额外实现 方法,序列化时会返回一个 对象。这个对象里包含了非常丰富的元信息,比如:SerializablewriteReplace()SerializedLambda
- 实现方法所在的类()getImplClass()
- 实现方法的名字(),例如getImplMethodName()"getUserName"
- 方法签名等
// 伪代码,展示核心思路
SerializedLambda serializedLambda = getSerializedLambda(func); // 通过序列化拿到元信息
String implMethodName = serializedLambda.getImplMethodName(); // "getUserName"
String fieldName = PropertyNamer.methodToProperty(implMethodName); // "userName"
// 再结合实体类上的 @TableField 注解或驼峰转下划线规则,
// 最终得到数据库列名 "user_name"
简化一下核心链路:
User::getUserName(方法引用)
↓ 编译为 Lambda,因实现了 Serializable
↓ 强转为 Serializable 后调用 writeReplace()
SerializedLambda(包含方法名等元信息)
↓ getImplMethodName() = "getUserName"
↓ 按照 JavaBean 规范去掉 get 前缀、首字母小写
"userName"(Java 属性名)
↓ 结合实体类字段上的 @TableField,或默认驼峰转下划线策略
"user_name"(数据库列名)
而且,为了避免每次都要走一遍反射+序列化(这个过程有一定性能开销),MP 内部会把已经解析过的方法引用做缓存( 里维护了缓存 Map),保证只解析一次,后续直接复用
面试背诵:MP 的 通过自定义 接口继承 ,利用“可序列化的 Lambda 表达式在序列化时会生成 对象”这一 JDK 特性,从中反射获取到方法引用对应的方法名(如 ),再按照 JavaBean 规范转换为属性名 ,最终结合驼峰转下划线规则或注解映射为数据库列名 。整个解析结果会被缓存,避免重复解析带来的性能损耗。
5.4 一个小坑: 不能传普通 Lambda 或方法体太复杂的引用SFunction
正确:简单的方法引用 wrapper.eq(User::getUserName, "张三");
错误:这样写无法反解析出字段名,会抛异常 wrapper.eq(u -> u.getUserName() + "1", "张三");
原因很简单:MP 是靠反解析方法名来推断字段的,一旦 Lambda 内部逻辑变复杂(不再是单纯的 getter 调用),拿到的可能就不再是一个能映射到属性的方法名了,自然就会报错或者匹配不到列。所以在 MP 里, 相关的方法引用,必须保持“纯 getter 引用”这种最简形式。
六、Q&A高频八股文
Q1:Lambda 表达式和函数式接口是什么关系?
A:Lambda 表达式是函数式接口的一种实例化方式,一个 Lambda 表达式必须对应到一个只有一个抽象方法的函数式接口上,否则编译器无法确定该把 Lambda 赋值给谁的抽象方法实现。
Q2:Lambda 底层是怎么实现的?和匿名内部类一样吗?
A:不一样。Lambda 编译后对应字节码里的 指令,运行时由 动态生成实现类;匿名内部类则是编译期就生成独立的 文件。
Q3:为什么 Lambda 里访问的局部变量要求是 final 或事实最终?
A:因为 Lambda 对外部局部变量是值拷贝而非引用捕获,如果允许后续修改,会导致 Lambda 内部值和外部真实值不一致,为避免这种隐患 Java 直接在编译期做了限制。
Q4:方法引用有几种形式?
A:四种——静态方法引用()、绑定实例的方法引用()、未绑定实例的方法引用(,第一个参数作为调用者)、构造方法引用()。
Q5:MyBatis Plus 的 LambdaQueryWrapper 是怎么拿到字段名的?
A:见上文 5.3 详解,核心是“可序列化 Lambda + 反解析方法名 + 缓存”。
Q6:QueryWrapper 和 LambdaQueryWrapper 该怎么选?
A:能确定实体类字段、追求类型安全和可重构性时优先用 ;如果是拼接原生 SQL 片段、字段来自多表关联查询等 Lambda 表达不了的复杂场景,退回用字符串版的 或 XML 手写 SQL。
七、总结
希望这篇文章能帮你把 Lambda 表达式和 MyBatis Plus 的结合原理讲清楚,无论是日常开发还是面试都能用得上。



