作者 | Hollis
在日常開發中,我們會經常要在類中定義布爾類型的變量,比如在給外部系統提供一個RPC接口的時候,我們一般會定義一個字段表示本次請求是否成功的。
關于這個”本次請求是否成功”的字段的定義,其實是有很多種講究和坑的,稍有不慎就會掉入坑裡,作者在很久之前就遇到過類似的問題,本文就來圍繞這個簡單分析一下。到底該如何定一個布爾類型的成員變量。
一般情況下,我們可以有以下四種方式來定義一個布爾類型的成員變量:
boolean success
boolean isSuccess
Boolean success
Boolean isSuccess
以上四種定義形式,你日常開發中最常用的是哪種呢?到底哪一種才是正确的使用姿勢呢?
通過觀察我們可以發現,前兩種和後兩種的主要差別是變量的類型不同,前者使用的是boolean,後者使用的是Boolean。
另外,第一種和第三種在定義變量的時候,變量命名是success,而另外兩種使用isSuccess來命名的。
首先,我們來分析一下,到底應該是用success來命名,還是使用isSuccess更好一點。
success 還是 isSuccess
到底應該是用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(TM) 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之間有何差別:
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 With Gson :{"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開發手冊這條規定的評價:
是以,在定義POJO中的布爾類型的變量時,不要使用isSuccess這種形式,而要直接使用success!
Boolean還是boolean?
前面我們介紹完了在success和isSuccess之間如何選擇,那麼排除錯誤答案後,備選項還剩下:
boolean success
Boolean success
那麼,到底應該是用Boolean還是boolean來給定一個布爾類型的變量呢?
我們知道,boolean是基本資料類型,而Boolean是包裝類型。關于基本資料類型和包裝類之間的關系和差別請參考[一文讀懂什麼是Java中的自動拆裝箱
](
http://www.hollischuang.com/archives/2700)那麼,在定義一個成員變量的時候到底是使用包裝類型更好還是使用基本資料類型呢?
我們來看一段簡單的代碼
/**
* @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時的明确語義。
後來,作者單獨和《阿裡巴巴Java開發手冊》、《碼出高效》的作者——孤盡 單獨1V1(qing) Battle(jiao)了一下。最終達成共識,還是盡量使用包裝類型。
但是,作者還是想強調一個我的觀點,盡量避免在你的代碼中出現不确定的null值。
null何罪之有?
關于null值的使用,我在
使用Optional避免NullPointerException、
9 Things about Null in Java等文中就介紹過。
null
是很模棱兩可的,很多時候會導緻令人疑惑的的錯誤,很難去判斷傳回一個
null
代表着什麼意思。
圖靈獎得主Tony Hoare 曾經公開表達過
null
是一個糟糕的設計。
我把 null 引用稱為自己的十億美元錯誤。它的發明是在1965 年,那時我用一個面向對象語言( ALGOL W )設計了第一個全面的引用類型系統。我的目的是確定所有引用的使用都是絕對安全的,編譯器會自動進行檢查。但是我未能抵禦住誘惑,加入了Null引用,僅僅是因為實作起來非常容易。它導緻了數不清的錯誤、漏洞和系統崩潰,可能在之後 40 年中造成了十億美元的損失。
當我們在設計一個接口的時候,對于接口的傳回值的定義,盡量避免使用Boolean類型來定義。大多數情況下,别人使用我們的接口傳回值時可能用
if(response.isSuccess){}else{}
的方式,如果我們由于忽略沒有設定
success
字段的值,就可能導緻NPE(java.lang.NullPointerException),這明顯是我們不希望看到的。
是以,當我們要定義一個布爾類型的成員變量時,盡量選擇boolean,而不是Boolean。當然,程式設計中并沒有絕對。
總結
本文圍繞布爾類型的變量定義的類型和命名展開了介紹,最終我們可以得出結論,在定義一個布爾類型的變量,尤其是一個給外部提供的接口傳回值時,要使用success來命名,阿裡巴巴Java開發手冊建議使用封裝類來定義POJO和RPC傳回值中的變量。但是這不意味着可以随意的使用null,我們還是要盡量避免出現對null的處理的。
來源 | HollisChuang's Blog