那些年困扰着我们的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到底意味着什么?
- 浅显的认识——null当然表示“值不存在”。
- 对内存管理有点经验的理解——null表示内存没有被分配,指针指向了一个空地址。
- 稍微透彻点的认识——null可能表示某个地方处理有问题了,也可能表示某个值不存在。
- 被虐千万次的认识——哎哟,又一个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特性引入到目前的项目中,一个长期配合的团队以及一门古老的编程语言都需要不断的注入新活力,否则不进则退。