Java函数式开发 Optional空指针处理_java

摘要

空闲时会抽空学习同在jvm上运行的Groovy和Scala,发现他们对null的处理比早期版本Java慎重很多。在Java8中,Optional为函数式编程的null处理给出了非常优雅的解决方案。本文将说明长久以来Java中对null的蹩脚处理,然后介绍使用Optional来实现Java函数式编程。

那些年困扰着我们的null

在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
   }
 }
}

然后,问题又来了:如果一个null表示一个空字符串,那”"表示什么?

然后惯性思维告诉我们,”"和null不都是空字符串码?索性就把判断空值升级了一下:

boolean checkNotBlank(Object obj){
 return null != obj && !"".equals(obj) ? true : false;
}
void do(){
 if(checkNotBlank(obj1)){
   if(checkNotNull(obj2)){
    //do something
   }
 }
}

有空的话各位可以看看目前项目中或者自己过往的代码,到底写了多少和上面类似的代码。

不知道你是否认真思考过一个问题:一个null到底意味着什么?

  1. 浅显的认识——null当然表示“值不存在”。
  2. 对内存管理有点经验的理解——null表示内存没有被分配,指针指向了一个空地址。
  3. 稍微透彻点的认识——null可能表示某个地方处理有问题了,也可能表示某个值不存在。
  4. 被虐千万次的认识——哎哟,又一个NullPointerException异常,看来我得加一个if(null != value)了。

回忆一下,在咱们前面码字生涯中到底遇到过多少次java.lang.NullPointerException异常?NullPointerException作为一个RuntimeException级别的异常不用显示捕获,若不小心处理我们经常会在生产日志中看到各种由NullPointerException引起的异常堆栈输出。而且根据这个异常堆栈信息我们根本无法定位到导致问题的原因,因为并不是抛出NullPointerException的地方引发了这个问题。我们得更深处去查询什么地方产生了这个null,而这个时候日志往往无法跟踪。

有时更悲剧的是,产生null值的地方往往不在我们自己的项目代码中。这就存在一个更尴尬的事实——在我们调用各种良莠不齐第三方接口时,说不清某个接口在某种机缘巧合的情况下就会返回一个null……

回到前面对null的认知问题。很多javaer认为null就是表示“什么都没有”或者“值不存在”。按照这个惯性思维我们的代码逻辑就是:你调用我的接口,按照你给我的参数返回对应的“值”,如果这条件没法找到对应的“值”,那我当然返回一个null给你表示没有“任何东西”了。我们看看下面这个代码,用很传统很标准的Java编码风格编写:

class MyEntity{
  int id;
  String name;
  String getName(){
   return name;
  }
}

// main
public 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值。

// main
public 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这玩意就是个坑。

托尼.霍尔(你不知道这货是谁吗?自己去查查吧)曾经说过:“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement.”(大意是:“哥将发明null这事称为价值连城的错误。因为在1965那个计算机的蛮荒时代,空引用太容易实现,让哥根本经不住诱惑发明了空指针这玩意。”)。

然后,我们再看看null还会引入什么问题。

看看下面这个代码:

String address = person.getCountry().getProvince().getCity();

如果你玩过一些函数式语言(Haskell、Erlang、Clojure、Scala等等),上面这样是一种很自然的写法。用Java当然也可以实现上面这样的编写方式。

但是为了完满的处理所有可能出现的null异常,我们不得不把这种优雅的函数编程范式改为这样:

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 的臭毛病会影响你很长的时间(记得在某国外社区,这被称为:面向entity开发)。

利用Optional实现Java函数式编程

好了,说了各种各样的毛病,然后我们可以进入新时代了。

早在推出Java SE 8版本之前,其他类似的函数式开发语言早就有自己的各种解决方案。下面是Groovy的代码:

String version = computer?.getSoundcard()?.getUSB()?.getVersion():"unkonwn";

Haskell用一个 Maybe 类型类标识处理null值。而号称多范式开发语言的Scala则提供了一个和Maybe差不多意思的Option[T],用来包裹处理null。

Java8引入了 java.util.Optional<T>来处理函数式编程的null问题,Optional<T>的处理思路和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!

 (可以把上面的代码copy到你的IDE中运行,前提是必须安装了JDK8。)

上面的代码中创建了2个Optional,实现的功能基本相同,都是使用Optional作为String的外壳对String进行截断处理。当在处理过程中遇到null值时,就不再继续处理。我们可以发现第二个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, Consumer<String> consumer) {
 consumer.accept(lowerCase(str));
 }
}
//输出
//hallo java!
//NaN

这样,我们可以动态的处理一个字符串,如果在任何时候发现值为null,则使用orElse返回预设默认的“NaN”。

总的来说,我们可以将任何数据结构用Optional包裹起来,然后使用函数式的方式对他进行处理,而不必关心随时可能会出现的null。

我们看看前面提到的Person.getCountry().getProvince().getCity()怎么不用一堆if来处理。

第一种方法是不改变以前的entity:

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作为每一次返回的外壳,如果有某个位置返回了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 {
 Optional<Country> country = Optional.empty();
}
class Country {
 Optional<Province> provinec;
}
class Province {
 Optional<City> city;
 Optional<City> getCity(){//用于::
 return city;
 }
}
class City {
 Optional<String> name;
}

第一种方法可以平滑的和已有的JavaBean、Entity或POJA整合,而无需改动什么,也能更轻松的整合到第三方接口中(例如spring的bean)。建议目前还是以第一种Optional的使用方法为主,毕竟不是团队中每一个人都能理解每个get/set带着一个Optional的用意。

Optional还提供了一个filter方法用于过滤数据(实际上Java8里stream风格的接口都提供了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会因为出现null值抛出空指针异常,而orElseThrow会在出现null时,抛出一个使用者自定义的异常。可以查看API文档来了解所有方法的细节。

写在最后的

Optional只是Java函数式编程的冰山一角,需要结合lambda、stream、Funcationinterface等特性才能真正的了解Java8函数式编程的效用。本来还想介绍一些Optional的源码和运行原理的,但是Optional本身的代码就很少、API接口也不多,仔细想想也没什么好说的就省略了。

Optional虽然优雅,但是个人感觉有一些效率问题,不过还没去验证。如果有谁有确实的数据,请告诉我。

本人也不是“函数式编程支持者”。从团队管理者的角度来说,每提升一点学习难度,人员的使用成本和团队交互成本就会更高一些。就像在传说中Lisp可以比C++的代码量少三十倍、开发更高效,但是若一个国内的常规IT公司真用Lisp来做项目,请问去哪、得花多少钱弄到这些用Lisp的哥们啊?

但是我非常鼓励大家都学习和了解函数式编程的思路。尤其是过去只侵淫在Java这一门语言、到现在还不清楚Java8会带来什么改变的开发人员,Java8是一个良好的契机。更鼓励把新的Java8特性引入到目前的项目中,一个长期配合的团队以及一门古老的编程语言都需要不断的注入新活力,否则不进则退。

以上就是对Java Optional 的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索java
java optional、java8 optional、java optional详解、java.util.optional、java optional类,以便于您获取更多的相关知识。

时间: 2024-10-28 10:52:46

Java函数式开发 Optional空指针处理_java的相关文章

Docker搭建前端Java的开发环境详解_java

一.解决的痛点       1.免搭建后端开发环境.       2.开发环境改变只需要改变镜像就能同步更新.       3.不需要eclipse等IDE工具.       4.切换开发项目 二.解决思路 利用docker启动Ubuntu镜像,在容器中搭建好项目需要的开发环境,使用挂载卷将本地代码挂载到容器中,使用容器中的环境编译运行代码,宿主机通过 docker 暴漏出的端口访问容器中的服务,这样前端的开发机上就只需要部署docker就搞定了. 三.关于docker 了解docker 本文并

经典再现 基于JAVA平台开发坦克大战游戏_java

一.需求描述 1.功能性需求 在功能需求分析阶段,我们的主要任务是指定系统必须提供哪些服务,定义软件完成哪些功能,提供给那些人使用,功能需求是软件开发的一项基本需求,是需求分析必不可少的一部分.坦克大战是一款经典游戏了,本游戏学习了一些前辈们的经验,整体来说讲,游戏分为敌我双方,主要参与战斗的坦克有玩家控制,敌人坦克可以智能随机出现在屏幕上,并且移动,发射一定数量的子弹:玩家可以在规定的区域内随意移动坦克,当有子弹击中玩家时,玩家死亡,游戏结束:敌人坦克智能运行,敌方坦克由于需要具有一定智能性,

Java函数式编程(五):闭包_java

使用词法作用域和闭包 很多开发人员都存在这种误解,认为使用lambda表达式会导致代码冗余,降低代码质量.恰恰相反,就算代码变得再复杂,我们也不会为了代码的简洁性而在代码质量上做任何妥协,下面我们就会看到. 在前面一个例子中我们已经可以重用lambda表达式了;然而,如果再匹配另外一个字母,代码冗余的问题很快又卷土重来了.我们先来进一步分析下这个问题,然后再用词法作用域和闭包来把它解决掉. lambda表达式带来的冗余 我们来从friends中过滤出那些以N或者B开头的字母.继续延用上面的那个例

实例解析观察者模式及其在Java设计模式开发中的运用_java

一.观察者模式(Observer)的定义: 观察者模式又称为订阅-发布模式,在此模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知.这通常透过呼叫各观察者所提供的方法来实现.此种模式通常被用来事件处理系统. 1.观察者模式的一般结构 首先看下观察者模式的类图描述: 观察者模式的角色如下: Subject(抽象主题接口):定义了主题类中对观察者列表的一系列操作, 包括增加,删除, 通知等. Concrete Subject(具体主题类): Observer(抽象

java微信企业号开发之开发模式的开启_java

首先说微信企业号的开发模式分为:编辑模式(普通模式)和开发模式(回调模式) ,在编辑模式下,只能做简单的自定义菜单和自动回复消息,要想实现其他功能还得开启开发者模式. 一.编辑模式和开发模式对消息的处理流程  1.编辑模式下,所有的业务流程都配置在微信服务器上,由它处理   2.开发模式,消息通过第三方服务器处理,最后经过微信服务器把消息发送给用户   开发模式能处理的消息比编辑模式多,所以要先开启开发模式才能开发更多功能. 二.开发模式的开启      在回调模式下,企业不仅可以主动调用企业号

java微信开发API第一步 服务器接入_java

微信开发API如何接入服务器,下面就为大家进行介绍 一.说明 * 本示例根据微信开发文档:http://mp.weixin.qq.com/wiki/home/index.html最新版(4/3/2016 5:34:36 PM )进行开发演示. * 编辑平台:myeclipse10.7+win32+jdk1.7+tomcat7.0  * 服务器:阿里云 windows server 2008 64bits * 平台要求:servlet使用注解方式,平台要求:j2ee6.0+.jdk6.0+.tom

Java Web开发环境配置详解_java

这是进行Java Web开发必备的一个过程,仅供新手参考,高手可以忽略! 先看看要安装的东西: jdk下载地址:http://www.jb51.net/softs/214120.html eclipse下载:http://www.jb51.net/softs/143046.html MySQL下载:http://www.jb51.net/softs/40589.html tomcat下载:http://www.jb51.net/softs/417569.html 各位也可以去官网上下载,版本不一

Java Web开发入门书籍实例解析(总结一)_java

一.基本概念 1.1.WEB开发的相关知识 WEB,在英语中web即表示网页的意思,它用于表示Internet主机上供外界访问的资源. Internet上供外界访问的Web资源分为: 1.静态web资源(如html 页面):指web页面中供人们浏览的数据始终是不变. 2.动态web资源:指web页面中供人们浏览的数据是由程序产生的,不同时间点访问web页面看到的内容各不相同. 静态web资源开发技术:Html 常用动态web资源开发技术:JSP/Servlet.ASP.PHP等 在Java中,动

Java Web开发防止多用户重复登录的完美解决方案_java

目前web项目中,很多情况都是可以让同一个账户信息在不同的登录入口登录这次,这样子就不那么美好了. 推荐阅读: Java 多用户登录限制的实现方法 现在有两种解决方案: 1.将用户的登录信息用一个标志位的字段保存起来,每次登录成功就标记1,注销登录就标记为0,当标记为1的时候不允许别人登录. 2.将用户的登录信息保存在application内置作用域内, 然后利用session监听器监听每一个登录用户的登录情况. 很显然,第一种方式 每次登录 都需要操作数据库,多了一些不必要的性能开销,而且在登