Java函数式开发——优雅的Optional空指针处理

摘要

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

那些年困扰着我们的null

在Java江湖流传着这样一个传说:直到真正了解了空指针异常,才能算一名合格的Java开发人员。在我们逼格闪闪的java码字符生涯中,每天都会遇到各种null的处理,像下面这样的代码可能我们每天都在反复编写:

稍微有点眼界javaer就去干一些稍有逼格的事,弄一个判断null的方法:

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

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

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

不知道你是否认真思考过一个问题:一个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编码风格编写:

这一段代码很简单,日常的业务代码肯定比这个复杂的多,但是实际上我们大量的Java编码都是按这种套路编写的,懂货的人一眼就可以看出最终肯定会抛出NullPointerException。但是在我们编写业务代码时,很少会想到要处理这个可能会出现的null(也许API文档已经写得很清楚在某些情况下会返回null,但是你确保你会认真看完API文档后才开始写代码么?),直到我们到了某个测试阶段,突然蹦出一个NullPointerException异常,我们才意识到原来我们得像下面这样加一个判断来搞定这个可能会返回的null值。

仔细想想过去这么些年,咱们是不是都这样干过来的?如果直到测试阶段才能发现某些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还会引入什么问题。

看看下面这个代码:

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

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

瞬间,高逼格的函数式编程Java8又回到了10年前。这样一层一层的嵌套判断,增加代码量和不优雅还是小事。更可能出现的情况是:在大部分时间里,人们会忘记去判断这可能会出现的null,即使是写了多年代码的老人家也不例外。

上面这一段层层嵌套的 null 处理,也是传统Java长期被诟病的地方。如果以Java早期版本作为你的启蒙语言,这种get->if null->return 的臭毛病会影响你很长的时间(记得在某国外社区,这被称为:面向entity开发)。

利用Optional实现Java函数式编程

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

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

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

Java8引入了 java.util.Optional<T>来处理函数式编程的null问题,Optional<T>的处理思路和Haskell、Scala类似,但又有些许区别。先看看下面这个Java代码的例子:

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

上面的代码中创建了2个Optional,实现的功能基本相同,都是使用Optional作为String的外壳对String进行截断处理。当在处理过程中遇到null值时,就不再继续处理。我们可以发现第二个Optional中出现s->null之后,后续的ifPresent不再执行。

注意观察输出的 //num3:,这表示输出了一个”"字符,而不是一个null。

Optional提供了丰富的接口来处理各种情况,比如可以将代码修改为:

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

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

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

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

这里用Optional作为每一次返回的外壳,如果有某个位置返回了null,则会直接得到”unkonwn”。

第二种办法是将所有的值都用Optional来定义:

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

Optional还提供了一个filter方法用于过滤数据(实际上Java8里stream风格的接口都提供了filter方法)。例如过去我们判断值存在并作出相应的处理:

现在我们可以修改为

到此,利用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一日一条(mjx_java)

原文发表时间:2016-09-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Phoenix的Android之旅

什么是策略模式

策略模式应该是Java设计模式中最简单的一种模式, 它的核心思想是,一个类的行为可以在运行时动态改变,有不同的实现逻辑。

933
来自专栏CDA数据分析师

飞跃式发展的后现代 Python 世界

如果现代Python有一个标志性特性,那么简单说来便是Python对自身定义的越来越模糊。在过去的几年的许多项目都极大拓展了Python,并重建了“Python...

2226
来自专栏数说工作室

撕数据! |【SAS Says·扩展篇】

【SAS Says·扩展篇】撕数据! | 4. call PRXPOSN() 0. 前集回顾 1. 新的问题 2. 初识 PRXPOSN() 3. 问题解决 -...

3527
来自专栏阿杜的世界

《重构》阅读笔记-代码的坏味道

开发者必须通过实践培养自己的经验和直觉,培养出自己的判断力:学会判断一个类内有多少个实例变量算是太大、学会判断一个函数内有多少行代码才算太长。

752
来自专栏老九学堂

学习C语言的用途以及如何快速掌握C语言

C是基础的语言 被广泛用于操作系统和编译器的开发 功能非常强 虽然现在不是最流行但它是 最基础的东西 也是比较好学的语言 如:金山的创始人江明 从30...

4447
来自专栏申龙斌的程序人生

零基础学编程033:字符串的split拆分与join连接

在《零基础学编程021:获取股票实时行情数据》这一节里,我们学了split()函数,可以将一个字符串切开。假设有一个历史行情字符串,信息包括:股票名称、开盘价、...

29911
来自专栏AzMark

Python函数的介绍

1626
来自专栏nimomeng的自我进阶

探索命名之美(二)

大家在编码或者读代码的过程中,对于什么样的命名是好的命名可能认知不是特别清晰,但是对于什么样的命名是坏的命名应该一目了然,他们包括:

832
来自专栏AzMark

Python字符串、循环及练习

2444
来自专栏姬小光

正则表达式是个啥

前些天有运营 MM 问小鸡君,正则表达式是个啥啊?懂技术的同学可能会想,你个运营管啥是正则表达式干啥?

1142

扫码关注云+社区

领取腾讯云代金券