java空指针异常处理-空指针异常怎么解决
Java界流传着这样一个传说:只有真正理解空指针异常,你才算是一个合格的Java开发者。 在我们浮华的java代码字符生涯中,每天都会遇到各种各样的null处理。 每天可能会重复写类似下面的代码:
if(null != obj1){ if(null != obje2){ // do something } }
登录复制
如果你有点javaer的眼光,可以做点比较有说服力的,想办法判断null
boolean checkNotNull(Object obj){ return null == obj ? false : true; } void do(){ if(checkNotNull(obj1)){ if(checkNotNull(obj2)){ //do something } } }
登录复制
那么java空指针异常处理,问题又来了:如果null表示空串,那么""是什么意思呢?
那么惯性思维告诉我们,""和null不都是空串码吗? 简单升级判断空值:
boolean checkNotBlank(Object obj){ return null != obj && !"".equals(obj) ? true : false; }void do(){ if(checkNotBlank(obj1)){ if(checkNotNull(obj2)){ //do something } } }
登录复制
有时间的话可以看看现在的项目或者自己过去的代码,写了多少和上面类似的代码。
不知道大家有没有认真想过一个问题:null是什么意思?
简单理解——null当然就是“该值不存在”的意思。
内存管理的一点心得——null表示内存还没有分配,指针指向空地址。
稍微透彻一点的理解——null可能表示某处处理有问题,也可能表示某个值不存在。
被虐过几千次的意识——哎哟,又是一个NullPointerException,看来我还得加一个if(null != value)。
回想一下,在我们之前的编码生涯中,我们遇到过多少次 java.lang.NullPointerException? NullPointerException 作为RuntimeException 级别的异常,不需要显示和捕获。 如果处理不当,我们会经常在生产日志中看到NullPointerException导致的各种异常堆栈输出。 并且根据这个异常堆栈信息,我们根本无法定位到问题的原因,因为并不是NullPointerException抛出的地方导致了问题。 我们要更深入的去查看这个null是从哪里产生的,而这个时候日志往往是无法追踪到的。
有时候更悲剧的是,产生空值的地方往往不在我们自己的项目代码中。 这是一个更尴尬的事实——当我们调用各种好的和坏的第三方接口时,不清楚某个接口会在某种巧合下返回null……
回到之前null的认知问题。 许多 javaer 认为 null 意味着“无”或“值不存在”。 按照这种惯性思维,我们的代码逻辑是:你调用我的接口,根据你给我的参数返回对应的“值”。 如果在这种情况下找不到对应的“值”,那我当然会返回一个null给你。 不再有“任何东西”。 让我们看一下下面的代码,它是用非常传统和标准的 Java 编码风格编写的:
class MyEntity{ int id; String name; String getName(){ return name; } }// mainpublic class Test{ public static void main(String[] args) final MyEntity myEntity = getMyEntity(false); System.out.println(myEntity.getName()); } private getMyEntity(boolean isSuc){ if(isSuc){ return new MyEntity(); }else{ return null; } } }
登录复制
这段代码很简单,日常的业务代码肯定比这复杂,但其实我们很多Java编码都是按照这个套路写的,懂货的人一看就知道是一个最后肯定会抛出NullPointerException。 但是我们在写业务代码的时候,很少会想到去处理这个可能的null(可能API文档已经写的很清楚了在某些情况下会返回null,但是你确定在开始写代码之前仔细阅读了API文档? ), 直到我们进行到某个测试阶段,突然跳出一个NullPointerException,我们才意识到,我们必须要像下面这样添加一个判断来去掉可能返回的null值。
// mainpublic class Test{ public static void main(String[] args) final MyEntity myEntity = getMyEntity(false); if(null != myEntity){ System.out.println(myEntity.getName()); }else{ System.out.println("ERROR"); } } }
登录复制
仔细想想这几年,我们都这样做过吗? 如果说一些null导致的问题直到测试阶段才发现,那么问题来了——那些优雅复杂的业务代码中,有多少null没有被正确处理?
对待null的态度,往往能体现一个项目的成熟和严谨。 比如Guava早在JDK1.6之前就提供了优雅的null处理方法,可见其底蕴之深。
幽灵般的 null 阻碍了我们的进步
如果你是一个专注于传统面向对象开发的Javaer,或许你已经习惯了null带来的问题。 但是早在很多年前,大神就说过null是个坑。
Tony Hall(你不知道这个人是谁吗?自己去找吧)曾经说过:“我称之为我的十亿美元错误。这是 1965 年空引用的发明。我无法抗拒诱惑放入空引用,仅仅是因为它很容易实现。” Jean 意识到,无法抗拒发明空指针的诱惑。”)。
那么,我们看看null会引入哪些问题。
看看这段代码:
String address = person.getCountry().getProvince().getCity();
登录复制
如果你玩过一些函数式语言(Haskell、Erlang、Clojure、Scala等),以上是一种很自然的写法。 当然,上面的写法也可以用Java来实现。
但是为了完美的处理所有可能的空异常,我们不得不把这个优雅的函数式编程范式改成这样:
if (person != null) { Country country = person.getCountry(); if (country != null) { Province province = country.getProvince(); if (province != null) { address = province.getCity(); } } }
登录复制
一瞬间,优质的函数式编程Java8又回到了10年前。 这样层层嵌套的判断,增加了代码量,不够优雅,还是小事。 更可能的情况是,大多数时候,人们会忘记判断可能出现的null,即使是写了多年代码的老人也不例外。
上段中的嵌套null处理也是传统Java长期被诟病的地方。 如果你使用早期版本的Java作为你的启蒙语言,这个get->if null->return的臭味会影响你很长一段时间(记得在国外的社区,这叫做:面向实体开发)。
使用Optional实现Java函数式编程
好吧,各种纠结都说了,然后我们就可以进入新时代了。
早在 Java SE 8 发布之前,其他类似的函数式开发语言就有了自己的各种解决方案。 这是 Groovy 代码:
String version = computer?.getSoundcard()?.getUSB()?.getVersion():"unkonwn";
登录复制
Haskell 使用 Maybe 类型类标志处理空值。 号称多范式开发语言的Scala提供了一个类似于Maybe的Option[T],用来包装和处理null。
Java8引入java.util.Optional来处理函数式编程的null问题。 Optional的处理思路和Haskell、Scala类似,但也有一些区别。 看看下面的 Java 代码示例:
public class Test { public static void main(String[] args) { final String text = "Hallo world!"; Optional.ofNullable(text)//显示创建一个Optional壳 .map(Test::print) .map(Test::print) .ifPresent(System.out::println); Optional.ofNullable(text) .map(s ->{ System.out.println(s); return s.substring(6); }) .map(s -> null)//返回 null .ifPresent(System.out::println); } // 打印并截取str[5]之后的字符串 private static String print(String str) { System.out.println(str); return str.substring(6); } } //Consol 输出 //num1:Hallo world! //num2:world! //num3: //num4:Hallo world!
登录复制
(你可以复制上面的代码在你的IDE中运行,前提是必须安装JDK8。)
上面代码中创建了两个Optional,实现的功能基本相同。 它们都使用 Optional 作为 String 的外壳来截断 String。 当在处理过程中遇到空值时,不再进行进一步的处理。 我们可以发现,在第二个Optional中出现s->null之后,后面的ifPresent就不再执行了。
注意输出//num3:,意思是输出一个“”字符,而不是null。
Optional 提供了丰富的接口来处理各种情况,例如,代码可以修改如下:
public class Test { public static void main(String[] args) { final String text = "Hallo World!"; System.out.println(lowerCase(text));//方法一 lowerCase(null, System.out::println);//方法二 } private static String lowerCase(String str) { return Optional.ofNullable(str).map(s -> s.toLowerCase()).map(s->s.replace("world", "java")).orElse("NaN"); } private static void lowerCase(String str, Consumerconsumer) { consumer.accept(lowerCase(str)); } } //输出 //hallo java! //NaN
登录复制
这样java空指针异常处理,我们就可以动态地处理一个字符串。 如果任何时候发现该值为 null,则使用 orElse 返回默认的“NaN”。
一般来说,我们可以用Optional包裹任何数据结构,然后用函数式的方式处理它,而不用关心随时可能出现的null。
让我们看看前面提到的 Person.getCountry().getProvince().getCity() 是如何在没有一堆 if 的情况下处理的。
第一种方法是不更改以前的实体:
import java.util.Optional;public class Test { public static void main(String[] args) { System.out.println(Optional.ofNullable(new Person()) .map(x->x.country) .map(x->x.provinec) .map(x->x.city) .map(x->x.name) .orElse("unkonwn")); } }class Person { Country country; }class Country { Province provinec; }class Province { City city; }class City { String name; }
登录复制
在这里,Optional 用作每次返回的 shell。 如果某个位置返回null,则直接获取“unkonwn”。
第二种方式是用Optional定义所有的值 :
import java.util.Optional;public class Test { public static void main(String[] args) { System.out.println(new Person() .country.flatMap(x -> x.provinec) .flatMap(Province::getCity) .flatMap(x -> x.name) .orElse("unkonwn")); } }class Person { Optionalcountry = Optional.empty(); }class Country { Optional provinec; }class Province { Optional city; Optional getCity(){//用于:: return city; } }class City { Optional name; }
登录复制
第一种方式无需任何改动即可与现有的JavaBean、Entity或POJA顺利集成,更容易集成到第三方接口(如spring beans)。 目前推荐关注第一种使用Optional的方法。 毕竟,不是团队中的每个人都能理解每个带有 Optional 的 get/set 的目的。
Optional还提供了过滤数据的filter方法(其实Java8中所有的流式接口都提供了filter方法)。 例如,以往我们判断值存在,并进行相应的处理:
if(Province!= null){ City city = Province.getCity(); if(null != city && "guangzhou".equals(city.getName()){ System.out.println(city.getName()); }else{ System.out.println("unkonwn"); } }
登录复制
现在我们可以修改为
Optional.ofNullable(province) .map(x->x.city) .filter(x->"guangzhou".equals(x.getName())) .map(x->x.name) .orElse("unkonw");
登录复制
至此,使用Optional进行函数式编程的介绍就完成了。 除了上面提到的方法,Optional还根据更多的需要提供了orElseGet、orElseThrow等方法。 orElseGet会因为空值抛出空指针异常,orElseThrow会在出现空值时抛出自定义异常。 有关所有方法的详细信息,请参阅 API 文档。
写在最后
Optional只是Java函数式编程的冰山一角。 需要结合lambda、stream、Funcationinterface等特性才能真正理解Java8函数式编程的实用性。 本来想介绍一下Optional的一些源码和运行原理,但是Optional本身代码很少,API接口也很少。 仔细想了想,也没什么好说的,就省略了。
Optional虽然优雅,但个人感觉效率上有一些问题,暂时还没有验证。 如果谁有实际数据,请告诉我。
我也不是“函数式编程支持者”。 从一个团队管理者的角度来看,每增加一次学习难度,使用人员和团队互动的成本就会更高。 就像传说中的,Lisp可以比C++少30倍的代码,开发效率更高,但是如果国内的常规IT公司真的用Lisp做项目,我去哪里找这些Lisp需要多少钱?基于软件? 伙计?
但是我非常鼓励大家去学习和理解函数式编程的思想。 尤其是对于过去只接触Java语言,还没有搞清楚Java8会带来什么变化的开发者来说,Java8是一个很好的机会。 更鼓励在当前项目中引入新的 Java8 特性。 一个长期合作的团队,一门古老的编程语言,需要不断注入新的活力,否则不进则退。