使用MyBatis-Plus时给SFunction接口创建属性数组以简化代码
文章目录
通过一个简单的实例介绍如何在使用MyBatis-Plus时给SFunction接口创建属性数组以简化代码,同时巩固下自己的Java基础知识。
背景
实体类User源码如下
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private int age;
private String email;
private String department;
private String phone;
}
接口类UserMapper源码如下,其集成了MyBatis-Plus的基础接口,以便后续可使用MyBatis-Plus进行快速开发。
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lucumt.entity.User;
public interface UserMapper extends BaseMapper<User> {
}
对应的测试代码如下,需要查询User类中的相关属性,可以看出第8行的代码长度很长,出现了滚动条导致阅读起来不太方便
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testQueryUsers() {
List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery().select(User::getId, User::getName, User::getAge, User::getEmail, User::getPhone, User::getDepartment));
Assertions.assertNotNull(userList);
}
}
虽然可以通过换行来消除滚动条,但本质上还是一行代码,当该类的属性很多时,若需要实现类似SELECT *的效果则会导致多个换行,还是不太便于阅读理解。
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testQueryUsers() {
List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery().select(User::getId, User::getName,
User::getAge, User::getEmail, User::getPhone, User::getDepartment));
Assertions.assertNotNull(userList);
}
}
自己期望将这些属性都放到一个数组中,在实现简化的同时也能便于阅读理解。
尝试
最开始自己很直接的想把上述属性提取到一个数组中,将代码修改如下
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testQueryUsers() {
SFunction<User, ?>[] properties = new SFunction[]{User::getId, User::getName,
User::getAge, User::getEmail, User::getPhone, User::getDepartment};
List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery().select(properties));
Assertions.assertNotNull(userList);
}
}
修改完毕后编译器立即显示编译错误,直接放置到数据中的方式不通。

将其修改为如下后,编译错误消失,此时虽然实现了将它们都放到数组中,但是每个属性都需要进行变量声明,代码篇幅较多,不是自己期望的最优解。
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testQueryUsers() {
SFunction<User, ?> getId = User::getId;
SFunction<User, ?> getName = User::getName;
SFunction<User, ?> getAge = User::getAge;
SFunction<User, ?> getEmail = User::getEmail;
SFunction<User, ?> getPhone = User::getPhone;
SFunction<User, ?> getDepartment = User::getDepartment;
SFunction<User, ?>[] properties = new SFunction[]{getId, getName, getAge, getEmail, getPhone, getDepartment};
List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery().select(properties));
Assertions.assertNotNull(userList);
}
}
分析
为啥第一种方式方式会报错,而第二种方式却能正常工作呢?
自己虽然知道这和Java和泛型相关的类型擦除,但限于自己孱弱的Java基础知识,并不能给出一个很好的解释。
我决定寻求更专业的协助,在Stackoverflow 上提出了一个问题,有2个大佬都从Java语言规范的角度给出了让人信服的回答。
首先前面图中出现的错误不是根本原因,基于我最开始的写法会导致多个错误,而IDE只会展示最顶层的错误,由此让人很迷惑,实际的原因是类型擦除。
在Array Initializers中有如下说明
Each variable initializer must be assignment-compatible (§5.2) with the array’s component type, or a compile-time error occurs.
翻译成人话就是数组中的每一个元素的类型都必须与数组类型兼容,否则会出现编译错误。
前述的数组创建方式
SFunction<User, ?>[] properties = new SFunction[]{User::getId, User::getName,
User::getAge, User::getEmail, User::getPhone, User::getDepartment};
实际上等价于下述代码
SFunction getId = User::getId;
SFunction getName = User::getName;
SFunction getAge = User::getAge;
SFunction getEmail = User::getEmail;
SFunction getPhone = User::getPhone;
SFunction getDepartment = User::getDepartment;
SFunction<User, ?>[] properties = new SFunction[]{getId, getName, getName, getEmail, getPhone, getDepartment};
而上述6行代码是无法编译通过的,其错误信息和前面截图一样,这是代码层面的问题溯源。
为什么那6行代码无法编译呢?需要在Java语言规范的Function Types中去寻找答案
The function type of the raw type of a generic functional interface I<…> is the erasure of the function type of the generic functional interface I<…>.
在这个回答中回答者结合上述说明给出了详细的解释:
由于存在继承关系,通用的SFunction<T, R>函数类型与Function<T, R>相同,其利用T作为输入,R作为输出,可以简单记做T->R。由于T和R都没有进行类型约束(即通过extends或super关键字进行约束),故它们都将被类型擦除为默认的Object类型,故SFunction<T, R>的函数类型实际上为Object->Object。
回到我们的问题中来User::getId是否兼容Object->Object呢?显然不是,其输入为 User,输出为Integer,实际的函数类型为User->Integer,故而不兼容。
基于上述分析以及对应回答者的建议,有如下几种方式:
-
修改源码以兼容函数类型
Function<? super Object, ? extends Object> properties = User::getId; -
采用
List代替数组List<SFunction<User, ?>> propertyList = Lists.newArrayList(User::getId, User::getName); -
显示的指定为实际的类型
// 通过变量实现 SFunction<User, ?>[] properties = new SFunction[] {val -> ((User) val).getId(), val -> ((User) val).getName() }; // 方法引用实现 SFunction<User, ?>[] propertiesss = new SFunction[]{(SFunction<User, ?>) User::getId, (SFunction<User, ?>) User::getName}; -
编写一个特定的方法用于进行类型转化
<T, R> SFunction<T, R> makeFunction(SFunction<T, R> x) { return x; }
改进
前面的4种方式,第1种由于需要修改源码不现实,故不考虑,第2种是修改数据类型与MyBatis-Plus的方法签名不匹配,也不考虑。
采用第3种方式可将代码修改如下,此种方式与之前的差别不大,还是无法避免强制转换
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testQueryUsers() {
SFunction<User, ?>[] properties = new SFunction[]{
(SFunction<User, ?>) User::getId,
(SFunction<User, ?>) User::getName,
(SFunction<User, ?>) User::getAge,
(SFunction<User, ?>) User::getEmail,
(SFunction<User, ?>) User::getPhone,
(SFunction<User, ?>) User::getDepartment
};
List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery().select(properties));
Assertions.assertNotNull(userList);
}
}
第4种方式相较于第3种只是提供了一个额外的方法进行统一的类型转化,但本质上和之前的一样
public class UserMapperTest {
@Resource
private UserMapper userMapper;
@Test
public void testQueryUsers() {
SFunction<User, ?>[] properties = new SFunction[]{
getProperty(User::getId),
getProperty(User::getName),
getProperty(User::getAge),
getProperty(User::getEmail),
getProperty(User::getPhone),
getProperty(User::getDepartment)
};
List<User> userList = userMapper.selectList(Wrappers.<User>lambdaQuery().select(properties));
Assertions.assertNotNull(userList);
}
private <T, R> SFunction<T, R> getProperty(SFunction<T, R> x) {
return x;
}
}
后记
前述问题的解决主要是依赖于Java语言规范,而自己却一直不怎么关注,我想起了另外一件事情。
虽然自己的在Stackoverflow上的积分也很多,但实话实说其中的大部分都是我在ChatGPT没有出来之前通过回答大量的JavaScript水出来的

如上图所示,截止到当前个人在Stackoverflow 上的总积分为13496,而自己回答了729个问题,排除掉自己提问带来的积分,平均每个回答带来的积分只有18.4,完全是数量压倒质量(quantity over quality)。
具体到相关问题和回答时,自己的表现也一般,如下图所示,得分最高的回答也才10个赞,只贡献了50分。

而自己关注的另一位大佬的回答概况如下,其平均得分是我的两倍

在具体问题和回答上的积分更是碾压我!

深入查看后可发现该大佬很多问题都是基于Java语言规范回答的,此种回答言简意赅,直击问题并且很容易获得很多赞,从而快速提高在Stackoverflow中的积分。

要想获得类似上面大佬一样的成就,必须要熟练掌握Java语言规范,而我们日常学习与开发中,更多关注的是Java的基本语法与使用场景,对Java语言规范则很少有人深入学习与研究。
结合当前Java就业市场的内卷以及大的环境,如果不能深入钻研并精通某一方面,很容易遭到淘汰,我们不仅要提高知识面的广度,更要熟练掌握某一个领域才能在残酷的竞争中立足。
Quality over Quantity!