1. 程式人生 > >為什麼阿里巴巴禁止開發人員使用isSuccess作為變數名

為什麼阿里巴巴禁止開發人員使用isSuccess作為變數名

在日常開發中,我們會經常要在類中定義布林型別的變數,比如在給外部系統提供一個RPC介面的時候,我們一般會定義一個欄位表示本次請求是否成功的。

關於這個”本次請求是否成功”的欄位的定義,其實是有很多種講究和坑的,稍有不慎就會掉入坑裡,作者在很久之前就遇到過類似的問題,本文就來圍繞這個簡單分析一下。到底該如何定一個布林型別的成員變數。

一般情況下,我們可以有以下四種方式來定義一個布林型別的成員變數:

boolean successboolean isSuccessBoolean successBoolean isSuccess

以上四種定義形式,你日常開發中最常用的是哪種呢?到底哪一種才是正確的使用姿勢呢?

通過觀察我們可以發現,前兩種和後兩種的主要區別是變數的型別不同,前者使用的是boolean,後者使用的是Boolean。

另外,第一種和第三種在定義變數的時候,變數命名是success,而另外兩種使用isSuccess來命名的。

首先,我們來分析一下,到底應該是用success來命名,還是使用isSuccess更好一點。

success 還是 isSucces

到底應該是用success還是isSuccess來給變數命名呢?從語義上面來講,兩種命名方式都可以講的通,並且也都沒有歧義。那麼還有什麼原則可以參考來讓我們做選擇呢。

在阿里巴巴Java開發手冊中關於這一點,有過一個『強制性』規定:

那麼,為什麼會有這樣的規定呢?我們看一下POJO中布林型別變數不同的命名有什麼區別吧。

class Model1  {   private Boolean isSuccess;   public void setSuccess(Boolean success) {       isSuccess = success;   }   public Boolean getSuccess() {       return isSuccess;   }}class Model2 {   private Boolean success;   public Boolean getSuccess() {       return success;   }   public void setSuccess(Boolean success) {       this.success = success;   }}class Model3 {   private boolean isSuccess;   public boolean isSuccess() {       return isSuccess;   }   public void setSuccess(boolean success) {       isSuccess = success;   }}class Model4 {   private boolean success;   public boolean isSuccess() {       return success;   }   public void setSuccess(boolean success) {       this.success = success;   }}

以上程式碼的setter/getter是使用Intellij IDEA自動生成的,仔細觀察以上程式碼,你會發現以下規律:

基本型別自動生成的getter和setter方法,名稱都是isXXX() 和setXXX() 形式的。
包裝型別自動生成的getter和setter方法,名稱都是getXXX() 和setXXX() 形式的。
既然,我們已經達成一致共識使用基本型別boolean來定義成員變量了,那麼我們再來具體看下Model3和Model4中的setter/getter有何區別。

我們可以發現,雖然Model3和Model4中的成員變數的名稱不同,一個是success,另外一個是isSuccess,但是他們自動生成的getter和setter方法名稱都是isSuccess和 setSuccess。

Java Bean中關於setter/getter的規範

關於Java Bean中的getter/setter方法的定義其實是有明確的規定的,根據JavaBeans™ Specification規定,如果是普通的引數,命名為propertyName,需要通過以下方式定義其setter/getter:

public <PropertyType> get<PropertyName>();public void set<PropertyName>(<PropertyType> a);

但是,布林型別的變數propertyName則是另外一套命名原則的:

public boolean is<PropertyName>();public void set<PropertyName>(boolean m);

通過對照這份JavaBeans規範,我們發現,在Model4中,變數名為isSuccess,如果嚴格按照規範定義的話,他的getter方法應該叫isIsSuccess。但是很多IDE都會預設生成為isSuccess。

那這樣做會帶來什麼問題呢。

在一般情況下,其實是沒有影響的。但是有一種特殊情況就會有問題,那就是發生序列化的時候。

序列化帶來的影響

關於序列化和反序列化請參考Java物件的序列化與反序列化。我們這裡拿比較常用的JSON序列化來舉例,看看常用的fastJson、jackson和Gson之間有何區別:

/**
* @author Hollis
*/
public class BooleanMainTest {   public static void main(String[] args) throws IOException {       //定一個Model3型別       Model3 model3 = new Model3();       model3.setSuccess(true);       //使用fastjson(1.2.16)序列化model3成字串並輸出       System.out.println("Serializable Result With fastjson :" + JSON.toJSONString(model3));       //使用Gson(2.8.5)序列化model3成字串並輸出       Gson gson =new Gson();       System.out.println("Serializable Result With Gson :"+gson.toJson(model3));       //使用jackson(2.9.7)序列化model3成字串並輸出       ObjectMapper om = new ObjectMapper();       System.out.println("Serializable Result With jackson :" +om.writeValueAsString(model3));   }}class Model3 implements Serializable {   private static final long serialVersionUID = 1836697963736227954L;   private boolean isSuccess;   public boolean isSuccess() {       return isSuccess;   }   public void setSuccess(boolean success) {       isSuccess = success;   }   public String getHollis(){       return "hollischuang";   }}

以上程式碼的Model3中,只有一個成員變數即isSuccess,三個方法,分別是IDE幫我們自動生成的isSuccess和setSuccess,另外一個是作者自己增加的一個符合getter命名規範的方法。

以上程式碼輸出結果:

Serializable Result With fastjson :{"hollis":"hollischuang","success":true}Serializable Result With Gson :{"isSuccess":true}Serializable Result With jackson :{"success":true,"hollis":"hollischuang"}

在fastjson和jackson的結果中,原來類中的isSuccess欄位被序列化成success,並且其中還包含hollis值。而Gson中只有isSuccess欄位。

我們可以得出結論:fastjson和jackson在把物件序列化成json字串的時候,是通過反射遍歷出該類中的所有getter方法,得到getHollis和isSuccess,然後根據JavaBeans規則,他會認為這是兩個屬性hollis和success的值。直接序列化成json:

{“hollis”:”hollischuang”,”success”:true}

但是Gson並不是這麼做的,他是通過反射遍歷該類中的所有屬性,並把其值序列化成json:

{“isSuccess”:true}

可以看到,由於不同的序列化工具,在進行序列化的時候使用到的策略是不一樣的,所以,對於同一個類的同一個物件的序列化結果可能是不同的。

前面提到的關於對getHollis的序列化只是為了說明fastjson、jackson和Gson之間的序列化策略的不同,我們暫且把他放到一邊,我們把他從Model3中刪除後,重新執行下以上程式碼,得到結果:

Serializable Result With fastjson :{"success":true}Serializable Result WithGson :{"isSuccess":true}Serializable Result With jackson :{"success":true}

現在,不同的序列化框架得到的json內容並不相同,如果對於同一個物件,我使用fastjson進行序列化,再使用Gson反序列化會發生什麼?

public class BooleanMainTest {   public static void main(String[] args) throws IOException {       Model3 model3 = new Model3();       model3.setSuccess(true);       Gson gson =new Gson();       System.out.println(gson.fromJson(JSON.toJSONString(model3),Model3.class));   }}class Model3 implements Serializable {   private static final long serialVersionUID = 1836697963736227954L;   private boolean isSuccess;   public boolean isSuccess() {       return isSuccess;   }   public void setSuccess(boolean success) {       isSuccess = success;   }   @Override   public String toString() {       return new StringJoiner(", ", Model3.class.getSimpleName() + "[","]")           .add("isSuccess=" + isSuccess)           .toString();   }}

以上程式碼,輸出結果:

Model3[isSuccess=false]

這和我們預期的結果完全相反,原因是因為JSON框架通過掃描所有的getter後發現有一個isSuccess方法,然後根據JavaBeans的規範,解析出變數名為success,把model物件序列化城字串後內容為{“success”:true}。

根據{“success”:true}這個json串,Gson框架在通過解析後,通過反射尋找Model類中的success屬性,但是Model類中只有isSuccess屬性,所以,最終反序列化後的Model類的物件中,isSuccess則會使用預設值false。

但是,一旦以上程式碼發生在生產環境,這絕對是一個致命的問題。

所以,作為開發者,我們應該想辦法儘量避免這種問題的發生,對於POJO的設計者來說,只需要做簡單的一件事就可以解決這個問題了,那就是把isSuccess改為success。

這樣,該類裡面的成員變數時success,getter方法是isSuccess,這是完全符合JavaBeans規範的。無論哪種序列化框架,執行結果都一樣。就從源頭避免了這個問題。

引用一下R大關於阿里巴巴Java開發手冊中這條規定的評價

https://www.zhihu.com/question/55642203):

所以,在定義POJO中的布林型別的變數時,不要使用isSuccess這種形式,而要直接使用success!

Boolean還是boolean?

前面我們介紹完了在success和isSuccess之間如何選擇,那麼排除錯誤答案後,備選項還剩下:

boolean successBoolean success

那麼,到底應該是用Boolean還是boolean來給定一個布林型別的變數呢?

我們知道,boolean是基本資料型別,而Boolean是包裝型別。關於基本資料型別和包裝類之間的關係和區別請參考一文讀懂什麼是Java中的自動拆裝箱

那麼,在定義一個成員變數的時候到底是使用包裝型別更好還是使用基本資料型別呢?

我們來看一段簡單的程式碼

/*** @author Hollis*/public class BooleanMainTest {   public static void main(String[] args) {       Model model1 = new Model();       System.out.println("default model : " + model1);   }}class Model {   /**    * 定一個Boolean型別的success成員變數    */   private Boolean success;   /**    * 定一個boolean型別的failure成員變數    */   private boolean failure;   /**    * 覆蓋toString方法,使用Java 8 的StringJoiner    */   @Override   public String toString() {       return new StringJoiner(", ", Model.class.getSimpleName() + "[","]")           .add("success=" + success)           .add("failure=" + failure)           .toString();   }}

以上程式碼輸出結果為:

default model : Model[success=null, failure=false]

可以看到,當我們沒有設定Model物件的欄位的值的時候,Boolean型別的變數會設定預設值為null,而boolean型別的變數會設定預設值為 false。

即物件的預設值是null,boolean基本資料型別的預設值是 false。

在阿里巴巴Java開發手冊中,對於POJO中如何選擇變數的型別也有著一些規定:

這裡建議我們使用包裝型別,原因是什麼呢?

舉一個扣費的例子,我們做一個扣費系統,扣費時需要從外部的定價系統中讀取一個費率的值,我們預期該介面的返回值中會包含一個浮點型的費率欄位。當我們取到這個值得時候就使用公式:金額*費率=費用 進行計算,計算結果進行劃扣。

如果由於計費系統異常,他可能會返回個預設值,如果這個欄位是Double型別的話,該預設值為null,如果該欄位是double型別的話,該預設值為0.0。

如果扣費系統對於該費率返回值沒做特殊處理的話,拿到null值進行計算會直接報錯,阻斷程式。拿到0.0可能就直接進行計算,得出介面為0後進行扣費了。這種異常情況就無法被感知。

這種使用包裝型別定義變數的方式,通過異常來阻斷程式,進而可以被識別到這種線上問題。如果使用基本資料型別的話,系統可能不會報錯,進而認為無異常。

以上,就是建議在POJO和RPC的返回值中使用包裝型別的原因。

在之前那篇文章的解析中,作者的觀點是,對於布林型別的變數,我認為可以和其他型別區分開來,作者並不認為使用null進而導致NPE是一種最好的實踐。因為布林型別只有true/false兩種值,我們完全可以和外部呼叫方約定好當返回值為false時的明確語義。

所以之前建議大家使用boolean來定義success變數,後來有讀者評論或者私下和我溝通,關於這部分內容又有了一些新的思考。

後來,作者單獨和《阿里巴巴Java開發手冊》、《碼出高效》的作者——孤盡 單獨1V1(qing) Battle(jiao)了一下。最終達成共識,還是儘量使用包裝型別。

但是,作者還是想強調一個我的觀點,儘量避免在你的程式碼中出現不確定的null值。

null何罪之有?

null是很模稜兩可的,很多時候會導致令人疑惑的錯誤,很難去判斷返回一個 null代表著什麼意思。

圖靈獎得主Tony Hoare 曾經公開表達過null是一個糟糕的設計。

我把 null 引用稱為自己的十億美元錯誤。它的發明是在1965 年,那時我用一個面嚮物件語言( ALGOL W )設計了第一個全面的引用型別系統。我的目的是確保所有引用的使用都是絕對安全的,編譯器會自動進行檢查。但是我未能抵禦住誘惑,加入了Null引用,僅僅是因為實現起來非常容易。它導致了數不清的錯誤、漏洞和系統崩潰,可能在之後 40 年中造成了十億美元的損失。

當我們在設計一個介面的時候,對於介面的返回值的定義,儘量避免使用Boolean型別來定義。大多數情況下,別人使用我們的介面返回值時可能用if(response.isSuccess){}else{}的方式,如果我們由於忽略沒有設定 success欄位的值,就可能導致NPE,這明顯是我們不希望看到的。

當然,程式設計中並沒有絕對。兩種方式是兩種思維方式,多提供一種思路,供讀者們在日常開發中自行選擇。

總結

本文圍繞布林型別的變數定義的型別和命名展開了介紹,最終我們可以得出結論,在定義一個布林型別的變數,尤其是一個給外部提供的介面返回值時,要使用success 來命名,阿里巴巴Java開發手冊建議使用封裝類來定義POJO和RPC返回值中的變數。但是這不意味著可以隨意的使用null,我們還是要儘量避免出現對null的處理的。

好啦,以上就是關於POJO中布林型別變數的正確定義姿勢。你用對了麼?