是时候优雅地和NullPointException说再见了
8月16日 相见欢投稿 NullPointException应该算是每一个码农都很熟悉的家伙了吧?谁的代码不曾抛过几个空指针异常呢。。。
比如:你写了段如下的代码:publicvoidgetCompanyFromEmployee(){EmployeeemployeegetEmployee();Companycompanyemployee。getTeam()。getDepartment()。getCompany();System。out。println(company);}privateEmployeegetEmployee(){EmployeeemployeenewEmployee();employee。setEmployeeName(JiaGouWuDao);employee。setTeam(newTeam(DevTeam4));}
运行程序,你可能就等不到你需要的结果,而是要喜提NullPointException了。。。
作为JAVA开发中最典型的异常类型,甚至可能是很多程序员入行之后收到的第一份异常大礼包类型。而NullPointException也似乎成为了一种魔咒,迫使程序员在敲出的每一行代码的时候都需要去思考下是否需要去做一下判空操作,久而久之,代码中便充斥着大量的null检查逻辑。
于是呢,上面的代码会变成下面这样:publicvoidgetCompanyFromEmployee(){EmployeeemployeegetEmployee();if(employeenull){dosomethinghere。。。}Teamteamemployee。getTeam();if(teamnull){dosomethinghere。。。}Departmentdepartmentteam。getDepartment();if(departmentnull){dosomethinghere。。。}Companycompanydepartment。getCompany();System。out。println(company);}
是不是大家的项目中都有见过这种写法的?每行代码中都流露着对NullPointException的恐惧有木有?是不是像极了一颗被深深伤害过的心在小心翼翼地保护着自己?
null的困扰
通过上面代码示例,我们可以发现使用null可能会带来的一系列困扰:空指针异常,导致代码运行时变得不可靠,稍不留神可能就崩了使代码膨胀,导致代码中充斥大量的null检查与保护,使代码可读性降低
此外,null还有一个明显的弊端:含义不明确,比如一个方法返回了null,调用方不清楚到底是因为逻辑有问题导致为null,还是说null其实也是一种可以接受的正常返回值类型?
所以说,一个比较好的编码习惯,是尽量避免在程序中使用null,可以按照具体的场景分开区别对待:
确定是因为代码或者逻辑层面处理错误导致的无值,通过throw异常的方式,强制调用方感知并进行处理对待如果null代表业务上的一种正常可选值,可以考虑返回Optional来替代。
当然咯,有时候即使我们自己的代码不返回null,也难免会遇到调用别人的接口返回null的情况,这种时候我们真的就只能不停的去判空来保护自己吗?有没有更优雅的应对策略来避免自己掉坑呢?下面呢,我们一起探讨下null的一些优雅应对策略。
Optional应对null处理Optional一定比returnnull安全吗
前面我们提到了说使用Optional来替代null,减少调用端的判空操作压力,防止调用端出现空指针异常。
那么,使用返回Optional对象就一定会比returnnull更靠谱吗?
答案是:也不一定,关键要看怎么用!
比如:下面的代码,getContent()方法返回了个Optional对象,然后testCallOptional()方法作为调用方,获取到返回值后的操作方式:publicvoidtestCallOptional(){OptionalContentoptionalgetContent();System。out。println(下面代码会报异常);try{【错误用法】直接从Optional对象中get()实际参数,这种效果与返回null对象然后直接调用是一样的效果Contentcontentoptional。get();System。out。println(content);}catch(Exceptione){e。printStackTrace();}System。out。println(上面代码会报异常);}privateOptionalContentgetContent(){returnOptional。ofNullable(null);}
上述代码运行之后会发现报错了:下面代码会报异常java。util。NoSuchElementException:Novaluepresentatjava。util。Optional。get(Optional。java:135)atcom。veezean。skills。optional。OptionalService。testCallOptional(OptionalService。java:47)atcom。veezean。skills。optional。OptionalService。main(OptionalService。java:58)上面代码会报异常
既然直接调用Optional。get()报错,那就是调用前加个判断就好咯?publicvoidtestCallOptional2(){OptionalContentoptionalgetContent();使用前先判断下元素是否存在if(optional。isPresent()){Contentcontentoptional。get();System。out。println(content);}}
执行一下,果然不报错了。但是,这样真的就是解决方法吗?这样跟直接返回null然后使用前判空(下面的写法)其实也没啥区别,也并不会让调用方使用起来更加的优雅与靠谱:publicvoidtestNullReturn2(){ContentcontentgetContent2();if(content!null){System。out。println(content。getValue());}}
那怎么样才是正确的使用方式呢,下面一起来看下。
全面认识下Optional创建Optional对象
Optional对象,可以用来表示一个T类型对象的封装,或者也可以表示不是任何对象。Optional类提供了几个静态方法供对象的构建:
方法名
功能含义描述
empty()
构造一个无任何实际对象值的空Optional对象(可以理解为业务层面的null)
of(Tt)
根据给定的对象,构造一个此对象的封装Optional对象,注意入参t不能为null,否则会空指针
ofNullable(Tt)
根据传入的入参t的值构造Optional封装对象,如果传入的t为null,则等同于调用empty()方法,如果t不为null,则等同于调用of(Tt)方法
在项目中,我们可以选择使用上面的方法,实现Optional对象的封装:publicvoidtestCreateOptional(){使用Optional。of构造出具体对象的封装Optional对象System。out。println(Optional。of(newContent(111,JiaGouWuDao)));使用Optional。empty构造一个不代表任何对象的空Optional值System。out。println(Optional。empty());System。out。println(Optional。ofNullable(null));System。out。println(Optional。ofNullable(newContent(222,JiaGouWuDao22)));}
输出结果:Optional〔Content{id111,valueJiaGouWuDao}〕Optional。emptyOptional。emptyOptional〔Content{id222,valueJiaGouWuDao22}〕
这里需要注意下of方法如果传入null会抛空指针异常,所以比较建议大家使用ofNullable方法,可以省去调用前的额外判空操作,也可以避免无意中触发空指针问题:
Optional常用方法理解
在具体讨论应该如何正确使用Optional的方法前,先来了解下Optional提供的一些方法:
方法名
含义说明
isPresent
如果Optional实际有具体对象值,则返回true,否则返回false。
ifPresent
这是一个函数式编程风格的API接口,入参是一个函数,即如果Optional对象有实际对象值,则会执行传入的入参函数逻辑,如果不存在实际对象值,则不会执行传入的入参函数逻辑。
get
返回Optional封装的实际对象T数据,注意,如果实际对象数据不存在,会抛异常而非返回null
orElse
与get方法类似,都是获取Optional实际的对象值,区别在于orElse必须传入一个默认值,当Optional没有实际值的时候返回默认值而非抛异常
orElseGet
可以理解为orElse方法的升级版,区别在于orElse仅允许传入一个固定的默认值,而orElseGet的入参是一个函数方法,当Optional无实际值时,会执行给定的入参函数,返回动态值。
orElseThrow
与orElse类似,区别在于如果没有获取到,会抛出一个指定的异常。
filter
判定当前Optional的实际对象是否符合入参函数的过滤规则,如果符合则返回当前Optional对象,如果不符合则返回空Optional
map
接收一个入参函数,允许将Optional中的实际对象值处理转换为另一实际对象值(这个入参函数的返回值为T),并生成返回此新类型的Optional对象,如果生成的新对象为null,则返回一个空Optional对象
flatMap
与map类似,区别点在于入参函数的返回值类型有区别(此处入参函数的返回值为Optional)
看到这里的map与flatMap方法,不知道大家会不会联想到Stream流对象操作的时候也有这两个方法的身影呢(不了解的同学可以戳这个链接抓紧补补课:吃透JAVA的Stream流操作)?的确,它们的作用也是类似的,都是用来将一个对象处理转换为另一个对象类型的:
对于Optional而言,map与flatMap最终的实现效果其实都是一样的,仅仅只是入参的要求不一样,也即两种不同写法,两者区别点可以通过下图来理解:
实际使用的时候,可以根据需要选择使用map或者flatMap:publicvoidtestMapAndFlatMap(){OptionalUseruserOptionalgetUser();OptionalEmployeeemployeeOptionaluserOptional。map(user{EmployeeemployeenewEmployee();employee。setEmployeeName(user。getUserName());map与flatMap的区别点:此处return的是具体对象类型});System。out。println(employeeOptional);OptionalEmployeeemployeeOptional2userOptional。flatMap(user{EmployeeemployeenewEmployee();employee。setEmployeeName(user。getUserName());map与flatMap的区别点:此处return的是具体对象的Optional封装类型returnOptional。of(employee);});System。out。println(employeeOptional2);}
从输出结果可以看出,两种不同的写法,实现是相同的效果:Optional〔Employee(employeeNameJiaGouWuDao)〕Optional〔Employee(employeeNameJiaGouWuDao)〕
Optional使用场景减少繁琐的判空操作
再回到本篇文章最开始的那段代码例子,如果我们代码里面不去逐个做判空保护的话,我们可以如何来实现呢?看下面的实现思路:publicvoidgetCompanyFromEmployeeTest(){EmployeeemployeeDetailgetEmployee();StringcompanyNameOptional。ofNullable(employeeDetail)。map(employeeemployee。getTeam())。map(teamteam。getDepartment())。map(departmentdepartment。getCompany())。map(companycompany。getCompanyName())。orElse(NoCompany);System。out。println(companyName);}
先通过map的方式一层一层的去进行类型转换,最后使用orElse去获取Optional中最终处理后的值,并给定了数据缺失场景的默认值。是不是看着比一堆if判空操作要舒服多了?
适用场景:
需要通过某个比较长的调用链路一层一层去调用获取某个值的时候,使用上述方法,可以避免空指针以及减少冗长的判断逻辑。
需要有值兜底的数据获取场景
编码的时候,经常会遇到一些数据获取的场景,需要先通过一些处理逻辑尝试获取一个数据,如果没有获取到需要的数据,还需要返回一个默认值,或者是执行另一处理逻辑继续尝试获取。
比如从请求头中获取客户端IP的逻辑,按照常规逻辑,代码会写成下面这样:publicStringgetClientIp(HttpServletRequestrequest){StringclientIprequest。getHeader(XForwardedFor);if(!StringUtils。isEmpty(clientIp)){returnclientIp;}clientIprequest。getHeader(XRealIP);returnclientIp;}
但是借助Optional来实现,可以这样写:publicStringgetClientIp2(HttpServletRequestrequest){StringclientIprequest。getHeader(XForwardedFor);returnOptional。ofNullable(clientIp)。orElseGet(()request。getHeader(XRealIP));}
适用场景:
优先执行某个操作尝试获取数据,如果没获取到则去执行另一逻辑获取,或者返回默认值的场景。
替代可能为null的方法返回值
下面是一段从项目代码中截取的片段:publicFileInfoqueryOssFileInfo(StringfileId){FileEntityentityfileRepository。findByIdAndStatus(fileId,0);if(entity!null){returnnewFileInfo(entity。getName(),entity。getFilePath(),false);}FileHistoryEntityhisEntityfileHisRepository。findByIdAndStatus(fileId,0);if(hisEntity!null){returnnewFileInfo(hisEntity。getName(),hisEntity。getFilePath(),true);}}
可以看到最终的return分支中,有一种可能会返回null,这个方法作为项目中被高频调用的一个方法,意味着所有的调用端都必须要做判空保护。可以使用Optional进行优化处理:publicOptionalFileInfoqueryOssFileInfo(StringfileId){FileEntityentityfileRepository。findByIdAndStatus(fileId,0);if(entity!null){returnOptional。ofNullable(newFileInfo(entity。getName(),entity。getFilePath(),false));}FileHistoryEntityhisEntityfileHisRepository。findByIdAndStatus(fileId,0);if(hisEntity!null){returnOptional。ofNullable(newFileInfo(hisEntity。getName(),hisEntity。getFilePath(),true));}returnOptional。empty();}
这样的话,就可以有效地防止调用端踩雷啦
适用场景:
实现某个方法的时候,如果方法的返回值可能会为null,则考虑将方法的返回值改为Optional类型,原先返回null的场景,使用Optional。empty()替代。
包装数据实体中非必须字段
首先明确一下,Optional的意思是可选的,也即用于标识下某个属性可有可无的特性。啥叫可有可无?看下面代码:publicclassPostDetail{privateSprivateUserpostUprivateSprivateOptionalDatelastModifyTimeOptional。empty();privateOptionalattachmentOptional。empty();}
上面是一个帖子详情数据类,对于一个论坛帖子数据而言,帖子的标题、内容、发帖人这些都是属于必须的字段,而帖子的修改时间、帖子的附件其实是属于可选字段(因为不是所有的帖子都会被修改、也不是所有帖子都会带附件),所以针对这种可有可无的字段,就可以声明定义的时候使用Optional进行封装。
使用Optional进行封装之后有两个明显的优势:强烈的业务属性说明,明确的让人知晓这个是一个可选字段,等同于数据库建表语句里面设置nullable标识一样的效果;调用端使用的时候也省去了判空操作。
适用场景:
数据实体定义的时候,对于可选参数,采用Optional封装类型替代。
使用抛异常替代returnnull
相比于返回一个Optional封装的对象,直接抛异常具有强烈的警醒意味,意在表达此处存在预期之外的不合理情况,要求编码的时候,调用端必须要予以专门处理。publicTeamgetTeamInfo()throwsTestException{EmployeeemployeegetEmployee();Teamteamemployee。getTeam();if(teamnull){thrownewTestException(teamismissing);}}
相比直接returnnull,显然抛异常的含义更加明确。
JDK与开源框架的实践
JDK提供的很多方法里面,其实都是遵循着本文中描述的这种返回值处理思路的,很少会看到直接返回null的不止JDK,很多大型的开源框架源码中,也很少会看到直接returnnull的情况。
比如com。sun。jmx。snmp。agent中的一段代码:publicSnmpMibSubRequestnextElement()throwsNoSuchElementException{if(iter0){if(handler。sublist!null){returnhlist。getSubRequest(handler);}}if(itersize)thrownewNoSuchElementException();SnmpMibSubRequestresulthlist。getSubRequest(handler,entry);}
再比如Spring中org。springframework。data。jpa。repository。support包下面的方法例子:publicOptionalTfindById(IDid){Assert。notNull(id,IDMUSTNOTBENULL);ClassTdomainTypegetDomainClass();if(metadatanull){returnOptional。ofNullable(em。find(domainType,id));}LockModeTypetypemetadata。getLockModeType();MapString,ObjecthintsgetQueryHints()。withFetchGraphs(em)。asMap();returnOptional。ofNullable(typenull?em。find(domainType,id,hints):em。find(domainType,id,type,hints));}
总结
好啦,关于编码中对null的一些应对处理策略与思路呢,这里就给大家分享到这里,希望可以对大家有所启发,通过不断的细节优化与改进,最终摆脱被空指针摆布的局面
那么,对上面提到的一些内容与场景,你是否也有遇到相关的情况呢?你是怎么处理的呢?欢迎多切磋交流下
此外:关于本文中涉及的演示代码的完整示例,我已经整理并提交到github中,如果您有需要,可以自取:https:github。comveezeanJavaBasicSkills
我是悟道,聊技术、又不仅仅聊技术
如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。
期待与你一起探讨,一起成长为更好的自己。
四川省攀枝花市开展天府科技云服务企业行活动正值2022年全国科普日来临之际,四川省攀枝花市科协、攀枝花钒钛高新技术产业园区管委会、攀枝花学院共同组织策划了天府科技云服务企业行活动,面向市唯一的国家级高新技术产业园区广大……
请小心面善心狠的人常说这4句口头禅,遇到了请警惕人这一生,会遇见千奇百怪的风景,会遇见形形色色的人。有人让你刻骨铭心,有人让你钦佩敬仰,有人让你遍体鳞伤,有人让你一夜成长。有的人用话救人,有的人用话杀人,从言语中就能判断,一……
省级医生不承认县医院的诊断,也不肯参考,来了就一大堆检查,你我是医院的上级医院是不承认下级医院的任何检查的第一上级医院的医疗器械肯定比下级医院更精细结果更准确第二就是怕有医疗纠纷因为如果你在下级医院做的结果上级医院不再做一次检查直接给你……
肚子变得又大又圆,体重却没增加是怎么回事?谢邀!这样说的话,多半是虚胖或者是脂肪堆积,但其他部位变瘦,造成了体重未增加的状况,如果您需要把肚子减下去的话,建议做做塑身减肥,不仅仅可以解决大肚腩的问题,还可以让你的身体凹……
又出大招!南京楼市,托累了11月财经新势力作者:子非鱼01南京楼市跌累了陈奕迅《红玫瑰》里有句歌词特别有意思,梦里梦到醒来不来的梦,形容的是男女爱情之间的无力感,想要拼命躲进梦里,拼命想要逃脱,越……
是不是直肠癌不管多少年后都可能转移和复发?是不是直肠癌不管多少年后都可能转移和复发?当然不是啊。这种问题想都不用想,基本上这种绝对化的说法(不管多少年,都,这种用词,就是绝对化)注定是不靠谱的,怎么可能都会转移复发,而……
肠胃不好的人手上有哪些表现?1)手掌处有红色斑点:手掌上有红色小斑点的人,一般都是由于胃热引起的,它影响消化器官正常工作,从而出现胃肠不适;2)手掌处有白色斑点:当手掌上出现2,3个白色斑点的时候,这可能……
癌症与肺结核哪个可怕?这个问题还是比较简单的肺结核显然没有癌症可怕在过去,医学还不发达,对于肺结核束手无策,发明出了各种偏方,最著名的就是人血馒头。但是,这些都治不好肺结核。所以,过去(……
什么病会引起后足疼?脚后跟疼痛是一种常见的症状,但有许多因素。脚后跟疼有时是由全身疾病引起的,例如风湿性关节炎或痛风,但只影响脚的局部症状。脚后跟疼痛最常见的局部原因包括:足底筋……
一颗曾经被认为价值700万亿美元的小行星未来的任务太空将前往小行星,以开采它们在太空中运输的大量材料。图片:艺术家对16精神(Psyche)对于未来的太空矿工来说,小行星的圣杯是16精神(Psyche),……
怎样自测肝毒?肝毒多会有什么表现?感谢邀请!肝毒其实是泛指一切对肝不好的有害物质,肝脏积毒的话,是藏不住的,身体上的一些变化会给我们提示。也就是说肝毒堆积太多的话,我们身体上会有相应的症状表现出来,想知道自己体……
为什么体重减了十多斤,却看不出来瘦了?先回答你:体重减了十多斤,缺看不出来瘦了,很正常。你这个问题,我在减肥开始的第一个月也遇到过。我是跳绳减肥,跳了两个月后,体重开始明显下降。不过看上去却没有多大变化。导致……