在日常开发中,我们经常在类中定义布尔变量。比如在对外提供RPC接口的时候,我们一般会定义一个字段来表示请求是否成功。关于“本次请求是否成功”这个字段的定义,其实有很多种特殊性和陷阱。一不小心就会掉坑里。笔者很早以前就遇到过类似的问题,所以这篇文章就来围绕这个做一下简单的分析。到底如何设置一个布尔型成员变量。一般来说,我们可以通过以下四种方式来定义一个布尔型成员变量:booleansuccessbooleanisSuccessBooleansuccessBooleanisSuccess以上四种定义形式,你在日常开发中最常用的是哪一种呢?哪一个是正确的使用姿势?通过观察,我们可以发现,前两者和后两者的主要区别在于变量的类型,前者使用boolean,后者使用boolean。另外,在定义第一种和第三种类型的变量时,变量名是success,而另外两种使用isSuccess命名。首先我们来分析一下,是应该以success命名,还是isSuccess比较好。success或isSuccess我应该使用success还是isSuccess来命名变量?从语义上来说,两种命名方式都是合理的,没有歧义。那么我们还有哪些原则可以参考让我们做出选择呢?阿里巴巴Java开发手册对这一点有一个“强制性”规定:那么,为什么会有这样的规定呢?下面我们看一下POJO中布尔变量的不同命名方式的区别。classModel1{privateBooleanisSuccess;publicvoidsetSuccess(Booleansuccess){isSuccess=success;}publicBooleangetSuccess(){returnisSuccess;}}classModel2{privateBooleansuccess;publicBooleangetSuccess(){returnsuccess;}publicvoidsetSuccess(Booleansuccess){this.success=success;}}classModel3{privatebooleanisSuccess;在以上代码是IntellijIDEA自动生成的。如果你仔细观察上面的代码,你会发现如下规则:基本类型自动生成的getter和setter方法的名称是isXXX()和setXXX()的形式。包装类型自动生成的getter和setter方法以getXXX()和setXXX()的形式命名。既然我们已经达成共识,使用基本类型boolean来定义成员变量,那么我们就来仔细看看Model3和Model4中setter/getter的区别。我们可以发现,虽然Model3和Model4中成员变量的名字不同,一个是success,一个是isSuccess,但是它们自动生成的getter和setter方法名是isSuccess和setSuccess。JavaBean中的setter/getter规范实际上对JavaBean中getter/setter方法的定义有明确的规定。根据JavaBeans(TM)Specification,如果是一个普通的参数propertyName,它的setter应该定义为如下方式/getter:publicget();publicvoidset(a);但是,布尔变量propertyName是单独定义的:publicbooleanis();publicvoidset(布尔值);通过对比这个JavaBeans规范,我们发现在Model4中,变量名是isSuccess,如果严格按照规范定义,它的getter方法应该叫isIsSuccess。但是许多IDE默认会生成isSuccess。那么这样做有什么问题呢?总的来说,其实没什么影响。但是有一种特殊情况会出现问题,那就是发生序列化的时候。序列化的影响关于序列化和反序列化,请参考Java对象的序列化和反序列化。下面以比较常用的JSON序列化为例,看看常用的fastJson、jackson和Gson的区别:setSuccess(true);//使用fastjson(1.2.16)将model3序列化为字符串输出System.out.println("SerializableResultWithfastjson:"+JSON.toJSONString(model3));//使用Gson(2.8.5)将model3序列化成字符串,输出Gsongson=newGson();System.out.println("SerializableResultWithGson:"+gson.toJson(model3));//使用jackson(2.9.7)将model3序列化为字符串并输出ObjectMapperom=newObjectMapper();System.out.println("SerializableResultWithjackson:"+om.writeValueAsString(model3));}}classModel3implementsSerializable{privatestaticfinallongserialVersionUID=1836697963736227954L;privatebooleanisSuccess;publicbooleanisSuccess(){returnisSuccess;}publicvoidsetSuccess(booleansuccess){isSuccess=success;}publicStringgetHollis()"上面代码的Model3,只有一个成员变量isSuccess,三个方法,分别是IDE自动为我们生成的isSuccess和setSuccess,以及另一种是作者自己添加的符合getter命名约定的方法。上面代码的输出结果:在fastjson和jackson的结果中,原来类中的isSuccess字段被序列化为success,其中也包含了hollis值。而Gson只有isSuccess字段。我们可以得出一个结论:fastjson和jackson在将对象序列化成json字符串的时候,是通过反射遍历类中所有的getter方法得到getHollis和isSuccess,然后根据JavaBeans的规则,他会认为这是两个的值hollis和success属性。直接序列化成json:{"hollis":"hollishuang","success":true}但是Gson不会这样做。他通过反射遍历类中的所有属性,并将其值序列化为json:{"isSuccess":true}可以看到,由于序列化工具不同,序列化采用的策略不同,所以对于同一个对象的序列化结果同一个班可能不一样。上面提到的getHollis的序列化只是为了说明fastjson、jackson和Gson在序列化策略上的区别。让我们暂时把他放在一边。我们将他从Model3中删除后,重新执行上面的代码。得到结果:SerializableResultWithfastjson:{"success":true}SerializableResultWithGson:{"isSuccess":true}SerializableResultWithjackson:{"success":true}现在不同的序列化框架得到的json内容是不一样的,如果对于相同的object,我用fastjson序列化,然后用Gson反序列化,会怎么样?publicclassBooleanMainTest{publicstaticvoidmain(String[]args)throwsIOException{Model3model3=newModel3();model3.setSuccess(true);Gsongson=newGson();System.out.println(gson.fromJson(JSON.toJSONString(model3),Model3.class));}}classModel3implementsSerializable{privatestaticfinallongserialVersionUID=1836697963736227954L;privatebooleanisSuccess;publicbooleanisSuccess(){returnisSuccess;}publicvoidsetSuccess(booleansuccess){isSuccess=success;}@OverridepublicStringtoString(){returnnewString,Joiner3.Name()."[","]").add("isSuccess="+isSuccess).toString();}}上面的代码,输出结果:Model3[isSuccess=false]这与我们预期的结果完全相反。原因是JSON框架扫描了所有的getter后发现了一个isSuccess方法,然后根据JavaBeans规范解析出变量名success,把模型对象序列化成字符串后,内容为{"success“:真的}。Gson框架根据json字符串{"success":true}解析后通过反射在Model类中查找success属性,但是Model类中只有isSuccess属性,所以,在最终反序列化的Model类对象中,isSuccess将使用默认值false。但是,一旦在生产环境中出现上面的代码,这绝对是一个致命的问题。所以,作为开发者,我们应该想办法尽可能的避免这种问题的发生。对于POJO设计者来说,他们只需要做一件简单的事情就可以解决这个问题,那就是改变就是成功到成功。这样,该类中的成员变量为success,getter方法为isSuccess,完全符合JavaBeans规范。无论使用何种序列化框架,执行结果都是一样的。只是从源头上避免这个问题。以下引用阿里巴巴Java开发手册(https://www.zhihu.com/question/55642203)提供的大学R的评论:因此,在POJO中定义布尔变量时,不要使用isSuccess的形式,并且直接使用成功!布尔值还是布尔值?前面我们已经介绍了如何在success和isSuccess之间进行选择,所以去掉错误答案后,剩下的选项就是:booleansuccessBooleansuccess那么,给一个布尔变量应该用boolean还是boolean呢?我们知道boolean是一种基本数据类型,Boolean是一种封装类型。基本数据类型和包装类的关系和区别可以参考一篇文章了解什么是Java中的自动拆箱。那么,在定义成员变量时,是使用包装类型好还是使用基本数据类型好呢?我们看一段简单的代码:/***@authorHollis*/publicclassBooleanMainTest{publicstaticvoidmain(String[]args){Modelmodel1=newModel();System.out.println("defaultmodel:"+model1);}}classModel{/***定义一个布尔型成功成员变量*/privateBooleansuccess;/***定义一个布尔型失败成员变量*/privatebooleanfailure;/***重写toString方法,使用Java8的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。如果扣费系统没有对rate的返回值做特殊处理,计算得到空值会直接报错阻塞程序。如果得到0.0,可以直接计算,接口为0后,扣手续费。这种异常是察觉不到的。这种使用wrapper类型定义变量的方式通过异常阻塞程序,然后可以识别到这种在线问题。如果使用基本数据类型,系统可能不会报错,然后认为没有异常。以上就是为什么建议在POJO和RPC返回值中使用包装器类型。但是关于这一点,笔者之前也有过不同的看法:对于Boolean变量,我觉得是可以区别于其他类型的。笔者并不认为使用null来引发NPE是一种完美的做法。因为boolean类型只有true/false两个值,当返回值为false时,我们可以和外部调用者在清晰的语义上达成共识。后来,笔者与《阿里巴巴Java开发手册》和《码出高效》的作者——古今进行了1V1(qing)Battle(jiao)。最终达成共识,尽量使用包装类型。不过,作者还是要强调我的一个观点,尽量避免在你的代码中出现不确定的空值。null什么是罪?关于null值的使用,我在使用Optional避免NullPointerException、Java中Null的9件事等文章中都有介绍,Null是很容易混淆的,很多时候会导致混淆错误。很难判断返回null意味着什么。图灵奖获得者TonyHoare曾公开表示null是一个糟糕的设计。我将空引用称为我自己的十亿美元错误。它是在1965年发明的,当时我用面向对象语言(ALGOLW)设计了第一个综合引用类型系统。我的目的是确保所有引用的使用都是绝对安全的,编译器会自动检查。但我无法抗拒包含Null引用的诱惑,仅仅是因为它很容易实现。它导致了无数的错误、错误和系统崩溃,在接下来的40年里可能会造成10亿美元的损失。我们在设计接口的时候,尽量避免使用布尔类型来定义接口的返回值。在大多数情况下,其他人在使用我们的接口返回值时可能会使用if(response.isSuccess){}else{}。如果忽略success字段的值,可能会导致NPE(java.lang.NullPointerException),这显然不是我们希望看到的。因此,当我们要定义Boolean类型的成员变量时,尽量选择boolean而不是Boolean。当然,编程没有绝对的事情。总结本文介绍了布尔变量定义的类型和命名。最后,我们可以得出结论,在定义布尔变量时,尤其是为外部提供的接口返回值时,应该以success命名。阿里巴巴Java开发手册推荐使用包装类在POJO和RPC返回值中定义变量。但是这并不意味着null可以随意使用,我们还是要尽量避免对null的处理。