欢迎光临
我们一直在努力

【Java.八股】一文彻底搞懂 Lambda 表达式 &&MyBatis Plus 实战

 🔥个人主页:代码不加冰(欢迎来访) 🎬作者简介: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 表达式在编译期会被转换为对 字节码指令的调用,具体的实现逻辑是在运行时通过 动态生成一个实现了目标函数式接口的类(并不是传统意义上的匿名内部类 文件)

流程简述:

  • 编译阶段:javac 把 Lambda 方法体编译成一个私有方法(放在宿主类里),并在调用处生成指令。
  • 运行阶段:第一次执行到 时,JVM 调用 这个引导方法,动态生成一个实现了对应函数式接口的类,并生成一个调用点。
  • 之后再次执行到该处,直接复用已生成的 CallSite,不会重复生成类。
  • 为什么这么设计

    • 避免了像匿名内部类那样,每写一个 Lambda 就在编译期生成一个 文件,减少类文件数量,加快启动加载速度;
    • 真正的类生成推迟到运行时,且只生成一次,兼具性能与灵活性。

    一句话总结:Lambda 表达式在字节码层面通过 invokedynamic 指令实现,运行时由 LambdaMetafactory 动态生成实现类,而不是编译期生成匿名内部类。

    4.2 Lambda 与匿名内部类的区别

    对比点匿名内部类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"

  • 如果数据库字段改名了,或者写代码时打错字,编译期完全发现不了,只有运行时才报错;
  • IDE 无法对字符串做重构(比如批量改名、查引用)。
  • 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"
    • 方法签名等
  • MP 的处理流程
  • // 伪代码,展示核心思路
    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 表达式的本质是函数式接口的简洁实例化写法,离开函数式接口它没有存在的意义;
  • 方法引用是 Lambda 在“方法体只是调用已有方法”这一特殊场景下的进一步简写,是理解 MP Lambda 用法的钥匙;
  • Lambda 底层依赖 + 动态生成实现类,而非传统匿名内部类的编译期生成方式;
  • MyBatis Plus 巧妙利用了“可序列化 Lambda 能反解析出方法元信息”这一 JDK 特性,实现了字段名到数据库列名的类型安全映射,这也是 最值得拿出来讲的技术亮点。
  • 希望这篇文章能帮你把 Lambda 表达式和 MyBatis Plus 的结合原理讲清楚,无论是日常开发还是面试都能用得上。

    赞(0)
    未经允许不得转载:171主机测评 » 【Java.八股】一文彻底搞懂 Lambda 表达式 &&MyBatis Plus 实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址