ITPub博客

首页 > 应用开发 > Java > 阿里为何禁止在对象中使用基本数据类型

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

原创 Java 作者:ehang 时间:2023-11-19 11:19:58 0 删除 编辑

大家好,我是一航!

前两天,因为一个接口的参数问题,和一位前端工程师产生了一些分歧,需求很简单:

根据一个数值类型(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)找我要

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

1 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

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(空指针)异常、 对象构建异常、 数据错误的问题;

继续看示例:

  • 映射对象

    数据库表:

    映射对象:

    
    
    @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 对象和数据库中的结果不一致的问题,这是绝对不允许的。

  • 解决办法

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

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次装箱动作;那这个过程必定会带来性能上的消耗;

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

好了,看到这里,阿里的这条约束应该就能彻底理解了;感谢你的三连...


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70035356/viewspace-2996033/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2023-11-17

  • 博文量
    14
  • 访问量
    22106