本文转载自GitChat网站的《解读《阿里巴巴 Java 开发手册》背后的思考》的其中一个章节,仅供做知识分享,如有侵权,告知立即删除。
对本文感兴趣的读者,可移步 https://gitbook.cn/books/5ca2da9a1763103ff10b0975/index.html 此处,为有价值的内容付费。

在日常开发中,我们会经常要在类中定义布尔类型的变量,比如在给外部系统提供一个 RPC 接口的时候,我们一般会定义一个字段表示本次请求是否成功的。
关于这个"本次请求是否成功"的字段的定义,其实是有很多种讲究和坑的,稍有不慎就会掉入坑里,作者在很久之前就遇到过类似的问题,本文就来围绕这个简单分析一下。到底该如何定一个布尔类型的成员变量。
一般情况下,我们可以有以下四种方式来定义一个布尔类型的成员变量:
booleansuccessbooleanisSuccessBoolean successBoolean isSuccess
以上四种定义形式,你日常开发中最常用的是哪种呢?到底哪一种才是正确的使用姿势呢?
通过观察我们可以发现,前两种和后两种的主要区别是变量的类型不同,前者使用的是 boolean,后者使用的是 Boolean。
另外,第一种和第三种在定义变量的时候,变量命名是 success,而另外两种使用 isSuccess 来命名的。
首先,我们来分析一下,到底应该是用 success 来命名,还是使用 isSuccess 更好一点。
success 还是 isSuccess
到底应该是用 success 还是 isSuccess 来给变量命名呢?从语义上面来讲,两种命名方式都可以讲的通,并且也都没有歧义。那么还有什么原则可以参考来让我们做选择呢。
在阿里巴巴 Java 开发手册中关于这一点,有过一个『强制性』规定:

那么,为什么会有这样的规定呢?我们看一下 POJO 中布尔类型变量不同的命名有什么区别吧。
classModel1{privateBoolean isSuccess;publicvoidsetSuccess(Boolean success){ isSuccess = success; }publicBooleangetSuccess(){returnisSuccess; } }classModel2{privateBoolean success;publicBooleangetSuccess(){returnsuccess; }publicvoidsetSuccess(Boolean success){this.success = success; } }classModel3{privatebooleanisSuccess;publicbooleanisSuccess(){returnisSuccess; }publicvoidsetSuccess(booleansuccess){ isSuccess = success; } }classModel4{privatebooleansuccess;publicbooleanisSuccess(){returnsuccess; }publicvoidsetSuccess(booleansuccess){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 get();publicvoidset( a);
但是,布尔类型的变量 propertyName 则是单独定义的:
publicboolean is();publicvoidset(boolean m);

通过对照这份 JavaBeans 规范,我们发现,在 Model4 中,变量名为 isSuccess,如果严格按照规范定义的话,他的 getter 方法应该叫 isIsSuccess。但是很多 IDE 都会默认生成为 isSuccess。
那这样做会带来什么问题呢。
在一般情况下,其实是没有影响的。但是有一种特殊情况就会有问题,那就是发生序列化的时候。
序列化带来的影响
关于序列化和反序列化请参考Java 对象的序列化与反序列化。我们这里拿比较常用的 JSON 序列化来举例,看看看常用的 fastJson、jackson和 Gson 之间有何区别:
publicclassBooleanMainTest{publicstaticvoidmain(String[] args)throwsIOException{//定一个Model3类型Model3 model3 =newModel3(); 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 =newGson(); System.out.println("Serializable Result With Gson :"+gson.toJson(model3));//使用jackson(2.9.7)序列化model3成字符串并输出ObjectMapper om =newObjectMapper(); System.out.println("Serializable Result With jackson :"+om.writeValueAsString(model3)); } }classModel3implementsSerializable{privatestaticfinallongserialVersionUID =1836697963736227954L;privatebooleanisSuccess;publicbooleanisSuccess(){returnisSuccess; }publicvoidsetSuccess(booleansuccess){ isSuccess = success; }publicStringgetHollis(){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 反序列化会发生什么?
publicclassBooleanMainTest{publicstaticvoidmain(String[] args)throwsIOException{ Model3 model3 =newModel3(); model3.setSuccess(true); Gson gson =newGson(); System.out.println(gson.fromJson(JSON.toJSONString(model3),Model3.class)); } }classModel3implementsSerializable{privatestaticfinallongserialVersionUID =1836697963736227954L;privatebooleanisSuccess;publicbooleanisSuccess(){returnisSuccess; }publicvoidsetSuccess(booleansuccess){ isSuccess = success; }@OverridepublicStringtoString(){returnnewStringJoiner(", ", 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 之间如何选择,那么排除错误答案后,备选项还剩下:
booleansuccessBoolean success
那么,到底应该是用 Boolean 还是 boolean 来给定一个布尔类型的变量呢?
我们知道,boolean 是基本数据类型,而 Boolean 是包装类型。关于基本数据类型和包装类之间的关系和区别请参考一文读懂什么是Java中的自动拆装箱
那么,在定义一个成员变量的时候到底是使用包装类型更好还是使用基本数据类型呢?
我们来看一段简单的代码
/**
* @author Hollis
*/publicclassBooleanMainTest{publicstaticvoidmain(String[] args){ Model model1 =newModel(); System.out.println("default model : "+ model1); } }classModel{/**
* 定一个Boolean类型的success成员变量
*/privateBoolean success;/**
* 定一个boolean类型的failure成员变量
*/privateboolean failure;/**
* 覆盖toString方法,使用Java 8 的StringJoiner
*/@OverridepublicStringtoString(){returnnewStringJoiner(", ", Model.class.getSimpleName() +"[","]") .add("success="+ success) .add("failure="+ failure) .toString(); } }
以上代码输出结果为:
defaultmodel : 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 的处理的。
网友评论