阿里為何禁止在物件中使用基本資料型別
大家好,我是一航!
前兩天,因為一個介面的引數問題,和一位前端工程師產生了一些分歧,需求很簡單:
根據一個數值型別(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部落格 ” ,連結:http://blog.itpub.net/70035356/viewspace-2996033/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- JAVA中基本資料型別和引用資料型別Java資料型別
- Java中的基本資料型別與引用資料型別Java資料型別
- JS 中普通物件資料型別的基本結構和操作JS物件資料型別
- swift基本資料型別使用-字典使用Swift資料型別
- JavaScript筆記5:計時器、物件、基本資料型別、引用資料型別JavaScript筆記物件資料型別
- 基本資料型別與API引用型別的使用資料型別API
- js中為什麼基本型別資料可以使用屬性和方法JS型別
- 基本資料型別資料型別
- js資料型別之基本資料型別和引用資料型別JS資料型別
- 基本資料型別與字串型別資料型別字串
- JavaScript基本資料型別JavaScript資料型別
- Java 基本資料型別Java資料型別
- Redis基本資料型別Redis資料型別
- Java -基本資料型別Java資料型別
- 003基本資料型別資料型別
- Java基本資料型別Java資料型別
- MySQL基本資料型別MySql資料型別
- golang資料型別基本介紹與使用Golang資料型別
- JS中其他資料型別轉為number資料型別的方法JS資料型別
- 常用資料庫基本資料型別資料庫資料型別
- Java中基本資料型別和包裝型別有什麼區別?Java資料型別
- 基本資料型別,for迴圈資料型別
- Redis資料型別基本操作Redis資料型別
- python基本資料型別Python資料型別
- 基本資料型別轉化資料型別
- 基本資料型別之字串資料型別字串
- Java的基本資料型別Java資料型別
- 三,基本資料型別(下)資料型別
- 二, 基本資料型別(上)資料型別
- 2-基本資料型別資料型別
- JavaScript 基本資料型別(轉)JavaScript資料型別
- 3. 基本資料型別資料型別
- 為什麼阿里巴巴禁止把SimpleDateFormat定義為static型別的?阿里ORM型別
- Redis 的 5 種資料型別的基本使用Redis資料型別
- Java集合不能存放基本資料型別,只存放物件的引用Java資料型別物件
- 自主資料型別:在TVM中啟用自定義資料型別探索資料型別
- oracle將表中date資料型別修改為timestamp資料型別Oracle資料型別
- 為什麼阿里巴巴禁止資料庫中做多表join?阿里資料庫