通过一个简单的实例介绍如何在使用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。由于TR都没有进行类型约束(即通过extendssuper关键字进行约束),故它们都将被类型擦除为默认的Object类型,故SFunction<T, R>的函数类型实际上为Object->Object

回到我们的问题中来User::getId是否兼容Object->Object呢?显然不是,其输入为 User,输出为Integer,实际的函数类型为User->Integer,故而不兼容。

基于上述分析以及对应回答者的建议,有如下几种方式:

  1. 修改源码以兼容函数类型

    Function<? super Object, ? extends Object> properties = User::getId;
    
  2. 采用List代替数组

    List<SFunction<User, ?>> propertyList = Lists.newArrayList(User::getId, User::getName);
    
  3. 显示的指定为实际的类型

    // 通过变量实现
    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};   
    
  4. 编写一个特定的方法用于进行类型转化

    <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积分

如上图所示,截止到当前个人在Stackoverflow 上的总积分为13496,而自己回答了729个问题,排除掉自己提问带来的积分,平均每个回答带来的积分只有18.4,完全是数量压倒质量(quantity over quality)。

具体到相关问题和回答时,自己的表现也一般,如下图所示,得分最高的回答也才10个赞,只贡献了50分。

个人Stackoverflow高赞回答

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

他人的Stackoverflow积分

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

他人Stackoverflow高赞回答

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

基于java规范回答问题

要想获得类似上面大佬一样的成就,必须要熟练掌握Java语言规范,而我们日常学习与开发中,更多关注的是Java的基本语法与使用场景,对Java语言规范则很少有人深入学习与研究。

结合当前Java就业市场的内卷以及大的环境,如果不能深入钻研并精通某一方面,很容易遭到淘汰,我们不仅要提高知识面的广度,更要熟练掌握某一个领域才能在残酷的竞争中立足。

Quality over Quantity!