阿里为何禁止在对象中使用基本数据类型

需求:

根据一个数值类型(type 取值范围1,2,3)来查询数据,如果没这个值,就是查询所有的数据;

这个需求很常见吧!但是在"没这个值"的问题上,想法不太一样:

  • 接口定义的规范是,查询所有时,那就不传这个type,我后端拿到的就是null,在MyBatis的配置里面,通过if标签,对type判空,来决定是否带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 方法的返回值和参数必须使用包装数据类型。
  • 【推荐】所有的局部变量使用基本数据类型。

下面就通过详细的示例,来说明一下为什么阿里的开发手册会有这样的约束;

1、Java 基本类型与包装类的关系

首先,我们需要了解清楚Java的基本数据类型对应的包装类,以及基本类型的默认值;

包装类在不实例化的前提下,默认值都是null

基本类型包装类字节数位数最小值最大值基本类型默认值
byteByte18-2^7(-128)2^7 - 1(127)(byte)0
shortShort216-2^152^15 - 1(short)0
intInteger432-2^312^31 - 10
longLong864-2^632^63 - 10L
floatFloat4321.4E - 4 (2^-149)3.4028235E38(2^128 - 1)0.0f
doubleDouble8644.9E- 324(2^-1074)1.7976931348623157E308(2^1024-1)0.0d
charCharacter216\u0000\uFFFF‘/uoooo’(null)
booleanBoolean180(false)1(true)false

2、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 方法及返回值的参数强制使用包装类的原因和上面是差不多的

3、ORM 关系映射对象必须使用包装类

数据库查询的映射对象,如果使用基础数据类型,就可能出现 NPE(空指针)异常、对象构建异常、数据错误的问题;

继续看示例:
映射对象
数据库表:

CREATE TABLE `user_info` (
  `id` int NOT NULL,
  `user_name` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `age` int DEFAULT NULL,
  `source` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

在这里插入图片描述
映射对象

@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 = userInfoMapper.selectById("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异常:

Caused by: org.apache.ibatis.reflection.ReflectionException: Error instantiating class com.zhubayi.mapper.entity.UserInfo with invalid types (Integer,String,int,Byte) or values (1,user1,null,49). Cause: java.lang.IllegalArgumentException
  • 问题二:数据错误

当对象中,没有 @AllArgsConstructor 注解,只带有 @Data 注解时,会生成所有属性的 Getter、Setter 方法;查询的结果会通过各个属性 Setter 方法基础赋值;

以上日志可以看出,id 为 1 的 age 数据库中查出来的是 null,不会调用对象的 Setter 方法赋值,可由于对象中的 age 是 int 基础数据类型,在对象创建之后,就赋予了初始值 0,最终造成使用者拿到的 UserInfo 对象和数据库中的结果不一致的问题,这是绝对不允许的。

JDBC Connection [HikariProxyConnection@1396794397 wrapping com.mysql.cj.jdbc.ConnectionImpl@241d1052] will not be managed by Spring
==>  Preparing: SELECT id,user_name,age,source FROM user_info WHERE id=?
==> Parameters: 1(String)
<==    Columns: id, user_name, age, source
<==        Row: 1, user1, null, 1
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5a899811]
2022-10-31 11:59:05.584  INFO 5536 --- [           main] com.zhubayi.Test01                       : 根据ID查询用户信息:UserInfo(id=1, userName=user1, age=0, source=49)
  • 解决办法

把基本数据类型换成包装类就好了;

4、局部变量推荐使用基础数据类型

【推荐】所有的局部变量使用基本数据类型。

那既然基本数据类型总是有问题,我们全部用包装类不就好了;但这里为什么局部变量又推荐使用基本数据类型呢?因为一旦涉及到运算,基础数据类型可以省去拆箱、装箱的动作,提高运行效率

什么是装箱和拆箱?

  • 装箱
    就是自动将基本数据类型转换为包装器类型;原理是调用包装类的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次装箱动作;那这个过程必定会带来性能上的消耗;

因此在局部变量中,合理的使用基本类型,可以有效的提高效率;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值