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

摘要

空闲时会抽空学习同在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 的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!

(0)

相关推荐

  • 详解Kotlin的空指针处理

    详解Kotlin的空指针处理 Kotlin的空指针处理相比于java有着极大的提高,可以说是不用担心出现NullPointerException的错误,kotlin对于对象为null的情况有严格的界定,编码的阶段就需要用代码表明引用是否可以为null,为null的情况需要强制性的判断处理. 咋看一下这些在java里面其实也有,问题是一般开发中不写也是可以的(大部分开发不会花很多时间考虑这些),等出了空指针错误再一个个打补丁.这样往往会遗漏很多空指针,后期的解决仅仅是做一个if判断,没有从根源解决

  • Java函数式编程(六):Optional

    选取单个元素 直觉来说选取单个元素肯定会比选取多个要简单得多,不过这里也存在一些问题.我们先看下一般的做法的问题是什么,然后再看下如何用lambda表达式来解决它. 我们先新建一个方法来查找一个以特定字母开头的元素,然后打印出来. 复制代码 代码如下: public static void pickName( final List<String> names, final String startingLetter) { String foundName = null; for(String

  • Java8中Optional类型和Kotlin中可空类型的使用对比

    本文主要给大家介绍了关于Java8中Optional类型和Kotlin中可空类型使用的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 在 Java 8中,我们可以使用 Optional 类型来表达可空的类型. package com.easy.kotlin; import java.util.Optional; import static java.lang.System.out; /** * Optional.ofNullable - 允许传递为 null 参数 *

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

    摘要 空闲时会抽空学习同在jvm上运行的Groovy和Scala,发现他们对null的处理比早期版本Java慎重很多.在Java8中,Optional为函数式编程的null处理给出了非常优雅的解决方案.本文将说明长久以来Java中对null的蹩脚处理,然后介绍使用Optional来实现Java函数式编程. 那些年困扰着我们的null 在Java江湖流传着这样一个传说:直到真正了解了空指针异常,才能算一名合格的Java开发人员.在我们逼格闪闪的java码字符生涯中,每天都会遇到各种null的处理,

  • Java 函数式编程要点总结

    目录 一.函数式概念 二.函数与方法 三.JDK函数基础 1.Lambda表达式 2.函数式接口 四.Optional类 1.Null判断 2.Optional应用 五.Stream流 六.源代码地址 一.函数式概念 函数式编程是一种结构化编程的范式,主要思想是把运算过程尽量写成系列嵌套的函数调用.函数编程的概念表述带有很抽象的感觉,可以基于案例看: public class Function01 {     public static void main(String[] args) {   

  • java 避免出现NullPointerException(空指针)的方法总结

    java 避免出现NullPointerException(空指针)的方法总结 Java应用中抛出的空指针异常是解决空指针的最好方式,也是写出能顺利工作的健壮程序的关键.俗话说"预防胜于治疗",对于这么令人讨厌的空指针异常,这句话也是成立的.值得庆幸的是运用一些防御性的编码技巧,跟踪应用中多个部分之间的联系,你可以将Java中的空指针异常控制在一个很好的水平上.顺便说一句,这是Javarevisited上的第二个空指针异常的帖子.在上个帖子中我们讨论了Java中导致空指针异常的常见原因

  • Java 8 开发的 Mybatis 注解代码生成工具

    MybatisAnnotationTools MybatisAnnotationTools 是基于 Java8 开发的一款可以用于自动化生成 MyBatis 注解类的工具,支持配置数据源.类路径,表名去前缀.指定类名前后缀等功能.同时支持 Java 8 和 Mybatis 3.5+ 的一些新特性,比如时间类 LocalDateTime/LocalDate .接口方法返回 Optional 等. 此工具生成的代码是基于注解的 Mybatis 接口方法,所以不会生成 XML 配置文件. 源码地址:G

  • 如何使用Java中的Optional

    NullPointerException是非常常见的异常.由于它,程序往往需要大量使用if-else代码块来处理空值,这使得代码看起来不简洁 优雅 ,且不方便自己和他人阅读.本文介绍如何用Optional类来处理null值问题. Optional类 先来看一段代码: String isocode = user.getAddress().getCountry().getIsocode().toUpperCase(); 这段代码在任何一个方法调用时,都有可能抛出NullPointerExceptio

  • 详解Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于某种语法或调用API去进行编程.例如,我们现在需要从一组数字中,找出最小的那个数字,若使用用命令式编程实现这个需求的话,那么所编写的代码如下: public static void main(String[] args) { int[] nums = new int[]{1, 2, 3, 4, 5,

  • 详解JAVA中的OPTIONAL

    一.概述 本质上,这是一个包含有可选值的包装类,这意味着 Optional 类既可以含有对象也可以为空. Optional 是 Java 实现函数式编程的强劲一步,并且帮助在范式中实现.但是 Optional 的意义显然不止于此. 我们从一个简单的用例开始.在 Java 8 之前,任何访问对象方法或属性的调用都可能导致NullPointerException: String isocode = user.getAddress().getCountry().getIsocode().toUpper

  • Java JDK8新增Optional工具类讲解

    Optional 空指针异Optional常是导致Java应用程序失败的最常见原因.以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码.受到Google Guava的启发,Optional类已经成为Java 8类库的一部分.Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null.Optional提供很多有用的方法,这样我们就不用显式进行空值检测. 1. 以前对nul

  • Kotlin 和 Java 混合开发入门教程

    目录 一.前沿 二.学习 Kotlin 前准备 三.Kotlin 语法简介 优秀的表达式 加强版 switch 模板字符串 空指针异常不存在了 编写单例类 扩展方法 运算符重载 四.Kotlin 与 Java 混合开发 五.Kotlin 与 Java 总结 一.前沿 如果你学习过其他的编程语言,你就会发现 Java 的语法很是哆嗦,可是我们为什么没有放弃 Java 这门编程语言呢?因为 JVM 是一个非常好的平台,而且 Java 程序员目前在中国所占的比重实在是太高了.这是历史包袱导致的.暂且不

  • 学会在Java中使用Optional功能

    目录 前言 Nullity Optional Class 客户责任 null Optional Objects 重要方法 创建方法 of ofNullable empty 实例方法 isPresent&isEmpty get orElse系列 orElseThrow系列 ifPresent系列 map flatMap filter 何时使用 返回值 字段 参数 替代方案 null 空对象 例外情况 结论 前言 尽管存在争议,但Optiont极大地改进了Java应用程序的设计.在本文中,我们将了解

随机推荐