深入解析 Java 方法引用:Lambda 表达式的进化之路

前言

方法引用是 Java 8 提供的一种新特性,它允许我们更简洁地传递现有方法作为参数。这项特性实际上是对 Lambda 表达式的一种补充,通过方法引用,我们可以直接引用现有方法,而无需编写完整的Lambda表达式。最近在使用方法引用的过程中有了一些感悟,这里希望以文章的形式记录下来,与大家分享。

1. 背景

最近在使用 Mybatis-plus 这个框架,这个框架能在 Mybatis 的基础上减少简单 SQL 的编写,直接使用Java 代码的方式拼接我们想要的 SQL,我为了研究怎样才能不写 SQL 也是在尽量多地翻看 Mybatis-plus 相关文档,尽量能用代码解决地绝不写 SQL。

1.1. LambdaQueryMapper

LambdaQueryMapper 是 MyBatis-Plus 提供的一种基于 Lambda 表达式的查询方式。通过使用 LambdaQueryMapper,我们可以利用 Lambda 表达式来编写类型安全的、更具可读性的查询代码,而无需担心手动编写 SQL 或者处理字符串连接等操作。下面是一个查询示例:

java 复制代码
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryWrapper;

public class UserService {
    private UserMapper userMapper;

    // 查询用户列表的方法示例
    public List<User> queryUserList(String username, Integer age, String email) {
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();

        // 构建查询条件
        queryWrapper.eq(User::getUsername, username)
                    .gt(age != null, User::getAge, age)
                    .like(email != null, User::getEmail, email);

        // 执行查询
        List<User> userList = userMapper.selectList(queryWrapper);
        return userList;
    }
}

在这个查询示例中,我们可以通过 User::getAge 指定 age 字段为查询条件,通过 User::getEmail来指定 email 为查询条件。这种形式的 Java 代码就叫做方法引用。

1.2. 函数式接口

说到方法引用,就不得不说函数式接口。在 java 中只有一个抽象方法的接口叫做函数式接口,每一种方法引用类型实际上对应一个函数式接口。

例如我们比较熟悉的 Runnable ,它其实就是一个函数式接口。它的定义如下:

java 复制代码
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

它就代表可以接收一个没有参数也没有返回值的方法:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.runnable(test::getXxx);
    }
    
    public void runnable(Runnable runnable){
        runnable.run();
    }
    
    public void getXxx(){
        System.out.println("run");
    }
}

同样的,如果我希望接收只有一个参数且只有一个返回值的方法进来,怎么做呢?Java 中也提供了这样的函数式接口 Function<T,R>:

java 复制代码
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

它接收一个参数 T,且返回一个类型 R,所以我们就可以这么用

java 复制代码
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.function(test::getXyy);
    }

    public <T,R> void function(Function<T,R> function){
        
    }
    
    
    public Integer getXyy(String s){
        return 1;
    }
}

1.3. 对于 User::getAge 疑惑

通过上面的叙述,我们知道只要在方法中声明函数式接口,我们就可以使用方法引用的形式将函数作为方法参数传入方法中。但是我在查看 LambdaQueryWrapper 源码的过程中方法,它在 eq、like 等方法中定义的参数类型为函数是接口是 SFunction<T,R>

java 复制代码
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}

也就是说它本质也是一个 Function<T,R> ,匹配的是一个参数以及一个返回值的方法,但是 User::getAge对应的 get 方法只有返回值而没有参数,这是怎么匹配上的?

2. 理解

经过一番查找资料,我终于理解了。下面就由小的来解释一下其中的原由。方法引用我们可以将其分成两部分,以 User::getAge 为例,前半部分是 User,它既可以是 User 类,也可以是 User 对象。后半部分是 getAge 它既可以是类方法(静态方法)也可以是对象方法(非静态方法),理论上有四种组合,即:

  • 对象-类方法
  • 对象-对象方法
  • 类-类方法
  • 类-对象方法

2.1. 对象::类方法

这种形式是不被允许的,会直接报错:

2.2. 对象::对象方法

如果是这种,其实就是直接匹配,也就是说,方法的类型必须和方法引用对应,以 Function<T,R> 为例,必须得是一个参数和一个返回值,代码如下:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        //引用为对象
        test.function(test::getXyy);
    }

    public <T,R> void function(Function<T,R> function){

    }

    
    public  Integer getXyy(String s){
        return 1;
    }
}

2.3. 类::类方法

这种和上面是一样的,也是要遵循参数和返回值相对应的规则:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        //引用变为类
        test.function(Test::getXyy);
    }

    public <T,R> void function(Function<T,R> function){

    }

    /**
     * 变为静态方法
     */

    public static Integer getXyy(String s){
        return 1;
    }
}

2.4. 类::对象方法

而对于类::对象方法这种形式,它就有点特殊了,它存在一个隐式转换,我们先看代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.function(Test::getXyy);
    }

    public <T,R> void function(Function<T,R> function){

    }

    /**
     * 无参数、一个返回值的对象方法
     */
    public Integer getXyy(){
        return 1;
    }
}

function 期待的参数类型是:一个参数、一个返回值的方法,然而对应的却是:无参数、一个返回值的方法,这是怎么回事呢?

原来,对于这种类型的方法引用存在一个隐式转化,即:Test::getXyy 等价于 (Test t) -> t.getXyy(),而 (Test t) -> t.getXyy()代表的就是:一个参数,一个返回值的方法,所以才能匹配 Function<T,R>函数式接口类型。

为什么它需要做这样的转换?本质上是由于类是无法调用对象方法,所以对于 类::对象方法这种形式的方法引用都需要进行这样的隐式转换。

相关推荐
reasonsummer几秒前
【办公类-115-05】20250920职称资料上传04——PDF和PDF合并PDF、图片和PDF合并PDF(十三五PDF+十四五图片)
java·python·pdf
Mcband2 分钟前
Apache Commons IO:文件流处理利器,让Java IO操作更简单
java·开发语言·apache
缺点内向2 分钟前
Java:将 Word 文档转换为密码保护的 PDF 文件
java·pdf·word
IT_陈寒27 分钟前
JavaScript性能飞跃:5个V8引擎优化技巧让你的代码提速300%
前端·人工智能·后端
骑士雄师38 分钟前
Java 泛型中级面试题及答案
java·开发语言·面试
Victor35640 分钟前
Redis(61)Redis的连接数上限是多少?
后端
Victor35644 分钟前
Redis(60) Redis的复制延迟如何优化?
后端
.格子衫.6 小时前
Spring Boot 原理篇
java·spring boot·后端
多云几多7 小时前
Yudao单体项目 springboot Admin安全验证开启
java·spring boot·spring·springbootadmin
Jabes.yang9 小时前
Java求职面试实战:从Spring Boot到微服务架构的技术探讨
java·数据库·spring boot·微服务·面试·消息队列·互联网大厂