百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-08-03 11:48 3 浏览

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

根据一个数值类型(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 方法的返回值和参数必须使用包装数据类型。
  • 【推荐】所有的局部变量使用基本数据类型。

最新阿里巴巴Java开发手册(黄山版),于2022年2月3日发布,有需要的朋友可以给我发(开发)两个字

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

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

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

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



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

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

好了,看到这里,阿里的这条约束应该就能彻底理解了

来源:https://mp.weixin.qq.com/s/X4T1Zk6NaPf_e_Tx_uJhaw

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...