深入解析 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 或者处理字符串连接等操作。下面是一个查询示例:

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 ,它其实就是一个函数式接口。它的定义如下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

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

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>:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

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

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>

@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> 为例,必须得是一个参数和一个返回值,代码如下:

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. 类::类方法

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

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. 类::对象方法

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

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>函数式接口类型。

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