前言
方法引用是 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>函数式接口类型。
为什么它需要做这样的转换?本质上是由于类是无法调用对象方法,所以对于 类::对象方法
这种形式的方法引用都需要进行这样的隐式转换。