阿里为何禁止在对象中使用基本数据类型
大家好,我是一航!
前两天,因为一个接口的参数问题,和一位前端工程师产生了一些分歧,需求很简单:
根据一个数值类型(type 取值范围1,2,3)来查询数据,如果没这个值,就是查询所有的数据;
这个需求很常见吧!但是在"没这个值"的问题上,想法不太一样:
-
我定义的规范是,没值的话,那就不传这个type,我后端拿到的就是null,在MyBatis的配置里面,通过if标签,就直接根据type判空,就变成了查询所有:
<select id="query" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from order_info where 1 = 1 <if test="type != null"> AND TYPE = #{type,jdbcType=INTEGER} </if> </select>
-
前端工程师的意思是,没有值的话,那我就给你传默认值了,数值类型的默认值是:0,后端就需要根据type是否是0来查询所有;
因此,MyBatis中 type 就需要加上大于0的判断
<if test="type != null and type > 0"> AND TYPE = #{type,jdbcType=INTEGER} </if>
虽然按着前端这样做,也确实可以实现,但是这个说法,是没有任何说服力的;因为 0 本身就是一个具体的值,并不符合 type 的取值范围,在Controler层的参数校验,就应该被干掉;如果某一天因为需求调整,将 0 也表示为某个具体类型之后,这里代码就需要做调整,同时查询所有和查询这个新增 0 的类型就会混淆,前端的展示也会受到影响;
0 和 null 对象在本质上还是有很大区别的;
在各执一词的背景下,我在技术交流群里面和各位大佬简单交流了一下,不想因为的我的执着影响到其他人;
大部分大佬的做法和我想的是一致的!最终也让前端按我的要求做了对应的调整;
那这个问题的根源,还是出在数值类型的默认值上;加上群里面几天讨论了几次相关的一些问题,这里就汇总说一下;
在阿里巴巴Java开发手册中有这样的一条规范,跟我们今天说的问题,也有一些关联:
- 【强制】所有的 POJO 类属性必须使用包装数据类型。
- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
- 【推荐】所有的局部变量使用基本数据类型。
最新阿里巴巴Java开发手册(黄山版),于2022年2月3日发布,有需要的朋友可以添加微信(mbb2100)找我要
下面就通过详细的示例,来说明一下为什么阿里的开发手册会有这样的约束;
Java 基本类型与包装类的关系
首先,我们需要了解清楚Java的基本数据类型对应的包装类,以及基本类型的默认值;
包装类在不实例化的前提下,默认值都是
null
基本类型 | 包装类 | 字节数 | 位数 | 最小值 | 最大值 | 基本类型默认值 |
---|---|---|---|---|---|---|
byte | Byte | 1 | 8 | -2^7(-128) | 2^7 - 1(127) | (byte)0 |
short | Short | 2 | 16 | -2^15 | 2^15 - 1 | (short)0 |
int | Integer | 4 | 32 | -2^31 | 2^31 - 1 | 0 |
long | Long | 8 | 64 | -2^63 | 2^63 - 1 | 0L |
float | Float | 4 | 32 | 1.4E - 4 (2^-149) | 3.4028235E38(2^128 - 1) | 0.0f |
double | Double | 8 | 64 | 4.9E - 324(2^-1074) | 1.7976931348623157E308(2^1024-1) | 0.0d |
char | Character | 2 | 16 | \u0000 | \uFFFF | '/uoooo'(null) |
boolean | Boolean | 1 | 8 | 0(false) | 1(true) | false |
POJO 类、RPC方法、返回值 强制使用包装类
在POJO 类、RPC方法返回值和参数需要强制使用包装类,如果使用基本数据类型,实例化出来的对象,就会初始化默认值;如果不赋值,对于使用者来说,根本就不知道这个值是因为默认值产生的,还是创建者设置的,从而带来后续的一些列问题;
举个例子
-
用户对象
@Data public class User { /** * 姓名 */ private String name; /** * 年龄 */ private Integer age; /** * 0:女 1:男 */ private byte gender; }
age 使用包装类,gender 性别用的基本数据类型(0表示女,1表示男)
-
接口
/** * 添加用户 * * @param user * @return */ @PostMapping("/add") private User add(@RequestBody User user) { log.info("添加的用户:{}", user); return user; }
-
测试
分别按以下的两种方式传参
当所有的请求参数都必传的话(右侧传参示例),不管属性是包装类、还是基本数据类型都没啥问题;
如果是左边的传参方式,只有名字必传,其他都没值,性别使用的是byte基础数据类型,User 对象创建之后,就会赋上默认值:0;0 表示女,这时候就可能让一个帅小伙儿无缘无故变成了女孩子;
RPC 方法及返回值的参数强制使用包装类的原因和上面是差不多的
ORM 关系映射对象必须使用包装类
数据库查询的映射对象,如果使用基础数据类型,就可能出现 NPE(空指针)异常
、对象构建异常
、数据错误
的问题;
继续看示例:
-
映射对象
数据库表:
映射对象:
@TableName(value = "user_info") @Data @AllArgsConstructor public class UserInfo implements Serializable { @TableField(exist = false) private static final long serialVersionUID = 1L; /** * 主键ID */ @TableId(value = "id", type = IdType.AUTO) private Integer id; /** * 用户名 */ @TableField(value = "user_name") private String userName; /** * 年龄 */ @TableField(value = "age") private int age; /** * 来源 */ @TableField(value = "source") private Byte source; }
-
查询方法
@Test void getById() { UserInfo userInfo = userInfoService.getById(1); log.info("根据ID查询用户信息:{}", userInfo); }
-
问题一:对象构建异常
当映射对象包含 @AllArgsConstructor(Lombok的注解) 时,会自动生成带所有属性的构造方法:
public UserInfo(final Integer id, final String userName, final int age, final Byte source) { this.id = id; this.userName = userName; this.age = age; this.source = source; }
查询 id 为 1 的数据时,由于 age 为 null,当通过以上构造方法实例化对象时,将一个 null 对象赋给一个基础数据类型,就会出现
IllegalArgumentException
异常:org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Error instantiating class com.ehang.mysql.mybatis.plus.generator.user.demain.UserInfo with invalid types (Integer,String,int,Byte) or values (1,一行Java 1,null,1). Cause: java.lang.IllegalArgumentException
-
问题二:数据错误
当对象中,没有 @AllArgsConstructor 注解,只带有 @Data 注解时,会生成所有属性的 Getter、Setter 方法;查询的结果会通过各个属性 Setter 方法基础赋值;
==> Preparing: SELECT id,user_name,age,source FROM user_info WHERE id=? ==> Parameters: 1(Integer) <== Columns: id, user_name, age, source <== Row: 1, 一行Java 1, null, 1 <== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1152bcd] 2022-10-30 12:57:09.308 INFO 504100 --- [ main] com.ehang.mysql.mybatis.plus.GetTest : 根据ID查询用户信息:UserInfo [Hash = 1795612732, id=1, userName=一行Java 1, age=0, source=1, serialVersionUID=1]
以上日志可以看出,id 为 1 的 age 数据库中查出来的是 null,不会调用对象的 Setter 方法赋值,可由于对象中的 age 是 int 基础数据类型,在对象创建之后,就赋予了初始值 0,最终造成使用者拿到的 UserInfo 对象和数据库中的结果不一致的问题,这是绝对不允许的。
-
解决办法
把基本数据类型换成包装类就好了;
局部变量推荐使用基础数据类型
【推荐】所有的局部变量使用基本数据类型。
那既然基本数据类型总是有问题,我们全部用包装类不就好了;但这里为什么局部变量又推荐使用基本数据类型呢?因为一旦涉及到运算,基础数据类型可以省去拆箱、装箱的动作,提高运行效率;
-
什么是装箱和拆箱?
-
装箱
就是自动将基本数据类型转换为包装器类型;原理是调用包装类的valueOf方法,如:
Integer.valueOf(1)
、Boolean.valueOf(true)
... -
拆箱
就是自动将包装器类型转换为基本数据类型;原理是是调用包装类的xxxValue方法(xxx表示类型),如:
public boolean booleanValue() { return value; }
-
还是以上周,一位小伙伴儿在群里面问的关于拆、装箱的效率问题为例:
由于没有他的源码和需求,这里我写了一个新的测试用例,来论证这个问题;
-
示例代码
public class Main { public static void main(String[] args) { int[] nums = new int[]{1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010}; long start = System.currentTimeMillis(); for (int n = 0; n < 10000000; n++) { for (int i = 0; i < nums.length; i++) { nums[i] = nums[i] + 1; nums[i] = nums[i] + 2; nums[i] = nums[i] + 3; } } System.out.printf("int 遍历10000000的耗时:%sms\n",System.currentTimeMillis()-start); start = System.currentTimeMillis(); Integer[] nums2 = new Integer[]{1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010}; for (int n = 0; n < 10000000; n++) { for (int i = 0; i < nums2.length; i++) { nums2[i] = nums2[i] + 1; nums2[i] = nums2[i] + 2; nums2[i] = nums2[i] + 3; } } System.out.printf("Integer 遍历10000000的耗时:%sms\n",System.currentTimeMillis()-start); } }
代码逻辑很简单,分别有一个 int 和 Integer 数组,里面的初始值一样,遍历
10000000
,每次将数组中的每个值都分别+1
、+2
、+3
再放回到数组中去; -
测试结果
int 遍历10000000的耗时:194ms Integer 遍历10000000的耗时:1965ms
根据耗时,会发现,int 数组的效率差不多是 Integer 数组的10倍
-
原因分析
明明初始值一样,计算出来的结果也一样,为什么效率会差了10倍之多?
主要的原因是出在以下的三行代码中:
nums[i] = nums[i] + 1; nums[i] = nums[i] + 2; nums[i] = nums[i] + 3;
如果是 int 数组,所有的值都是保存在栈中;不会有任何拆、装箱的动作;取值、计算、再赋值的过程也就是一气呵成,非常丝滑;
但是如果是 Integer 数组,
nums[i] = nums[i] + 1;
这行代码,计算过程如下:1. 在 nums[i] 中取出 Integer; 2. 将取出的值拆箱;`intValue`方法; 3. 拆箱后的 int 与 1 做相加运算,得到计算结果; 4. 将计算结果的 int 装箱生成 Integer 对象(主要耗时的地方); 5. 将结果放到 nums[i] 中。
由于做了3次运算,意味着每次循环都会将上面步骤重复3次;并经历了3次拆箱、3次装箱动作;那这个过程必定会带来性能上的消耗;
因此在局部变量中,合理的使用基本类型,可以有效的提高效率;
好了,看到这里,阿里的这条约束应该就能彻底理解了;感谢你的三连...