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. —Tony Hoare
有人说,当你处理过了空指针异常才真正成为一个Java开发者。抛开玩笑话不谈,空指针确实是很多bug的根源。Java SE 8引入了一个新的叫做java.util.Optional
的类来缓解这个问题。
我们首先看看空指针有什么危险,Computer是一个嵌套的对象,如图:
下面的代码有什么潜在的问题呢?
String version = computer.getSoundcard().getUSB().getVersion();
貌似可行,但是,很多电脑(比如 Raspberry Pi)并没有Soundcard,因此调用getSoundcard会发生什么?毫无疑问,结果自然是在运行时给你抛出一个NullPointException
,然后终止程序的执行。
如何避免上面的空指针异常呢?一般的做法就是在调用方法之前进行检测:
String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}
但是,上面嵌套if检测的代码确实不怎么好看。但是没办法,我们需要很多这样死板的没什么意义的代码来避免碰到NullPointException
。更恼火的是,这部分代码成了我们业务逻辑的一部分,还降低了代码的可读性。
万一我们忘记对某个可能为null的对象进行非空检测怎么办?使用null来说明某个值缺失是一种错误的方式, 下文将说明这个问题并给出更好的解决办法。
先看看别的编程语言是如何处理这个问题的。
Null的替代物
Grovvy语言有一个?.
的操作符,可以安全地处理潜在可能的空引用(C#即将包含这个特性,Java7曾被建议引入这个但是并没有发布。)它是这么用的:
String version = computer?.getSoundcard()?.getUSB()?.getVersion();
如果getSoundcard()
,getUSB()
,getVersion
任意一个返回null,变量version就被赋值为null,不需要额外的复杂的嵌套检测。更好的是,Grovvy还有一个Elvis操作符:?:
,可以给类似上面的表达式提供默认值。下面的表达式如果?. 返回了null那么变量version会被赋值为"UNKNOW":
String version = computer?.getSoundcard()?.getUSB()?.getVersion() ?: "UNKNOWN";
其他的一些函数式编程语言,比如Haskell, Scala,使用了一种别的方式。Haskell有一个Maybe型态,这个型态代表了一种有可选值的类型。Maybe形态的值可能包含一个给定类型的值或者是Nothing(译者注:代表没有值),完全没有空指针的概念。Scala有一种类似的叫做Option[T]的东西来代表类型T的某一个值存在或者没有。因此,你必须显式检测这个值是否存在,如果不存在就不能使用任何Option类型的操作符;这样由于Scala的类型系统,你永远也不会忘记对于空指针的检测。
貌似有点扯远了,那么,Java8给我们提供了什么呢?
果壳里的Optional
受到Haskell和Scala的启发,Java8引入了一个叫做java.util.Optional<T>
的类,这一个包含一个可选值的类型,你可以把它当作包含单个值的容器——这个容器要么包含一个值要么什么都没有,如下图:
我们在数据模型里面引入Optional:
public class Computer {
private Optional<Soundcard> soundcard;
public Optional<Soundcard> getSoundcard() { ... }
...
}
public class Soundcard {
private Optional<USB> usb;
public Optional<USB> getUSB() { ... }
}
public class USB{
public String getVersion(){ ... }
}
用上面的代码,我们一眼就可以看出来一个computer有没有soundcard(他们是optioal,可选的),更进一步,一个声卡也有一个可选的USB端口;新的模型能清晰地反映出一个给定的值是有可能不存在的。这种做法在某些库里面也存在,比如Guava
我们能用Optional对象干什么?Optional对象包含了一些方法来显式地处理某个值是存在还是缺失,Optional类强制你思考值不存在的情况,这样就能避免潜在的空指针异常。
值得一提的是,设计Optional类的目的并不是完全取代null, 它的目的是设计更易理解的API。通过Optional,可以从方法签名就知道这个函数有可能返回一个缺失的值,这样强制你处理这些缺失值的情况。
Optional的正确打开方式
废话扯了这么多,来点实际的例子吧!首先来看看如何使用Optional类来实现传统的空指针检测:
String name = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
如果无法理解这段代码,可以复习Java8的lambda和方法引用,见Java8 Lambdas 以及stream pipelining概念,见Processing Data with Java SE 8 Steams
创建Optional对象
如何创建Optional对象呢,有下面几种方式:
空的Optional
Optional<Soundcard> sc = Optional.empty();
包含非空值的Optional
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);
一旦soundcard是null,这段代码会立即抛出一个NullPointException(而不是等你以后你访问这个空的soundcard对象的时候)
可能为空的Optional
Optional<Soundcard> sc = Optional.ofNullable(soundcard);
如果soundcard是null那么这个Optional将会是empty.
值存在的时候进行进一步的操作
现在你有了一个Optional对象,你可以显式地处理值存在或者不存在的情况,再也不用想这样如履薄冰地进行空指针检测了:
SoundCard soundcard = ...;
if(soundcard != null){
System.out.println(soundcard);
}
现在,可以使用ifPresent()
方法,如下:
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);
现在,你再也不用显示地进行非空检测了,类型系统帮你干了这件事。如果Optional是empty,上面的代码就不会执行打印了。
你也可以使用isPresent()
方法检查某个值是否存在,另外,get 方法可以返回Optional容器里面包含的那个对象,如果没有这个对象,get方法会立即抛出一个NoSuchElementException,这两个方法可以结合起来:
if(soundcard.isPresent()){
System.out.println(soundcard.get());
}
但是,并不提倡这样使用Optional。(这么做跟null检测有什么区别?),下面有一些惯用手法,我们来看一下。
默认值和默认操作
在某个操作返回空的时候给出一个默认值也是一个典型的场景,通畅的做法是使用三目运算符(?:
)
Soundcard soundcard = maybeSoundcard != null ?
maybeSoundcard : new Soundcard("basic_sound_card");
可以使用Optional对象的ifElse
方法改进这个代码:
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
如果你想在空值的时候抛出一个异常,可以使用ifElseThrow
方法:
Soundcard soundcard =
maybeSoundCard.orElseThrow(IllegalStateException::new);
使用filter过滤特定值
很多时候你需要调用某个对象的方法并且检查它的一些属性。例如:你可能需要检测一个USB的端口是否是一个特定的版本;如果需要避免空指针异常,通畅的方式是检测非空然后调用getVersion
方法,如下:
USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
System.out.println("ok");
}
使用Optional的filter可以这么干:
Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
filter
方法带有一个Predicate
的参数,如果Optional容器里面的对象存在并且满足这个predicate,那么filter返回那个对象,否则就返回empty的Optional。(跟Stream接口的filter类似)
使用map转换值
另外一个比较常见的场景是需要从某个对象里面提取出特定的值。例如:从一个Soundcard对象里面取出一个USB对象然后检测这个usb对象是否是正确的版本。通常可以这么写:
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}
使用Optional的map方法,如下:
Optional<USB> usb = maybeSoundcard.map(Soundcard::getUSB);
Optional容器里面的值被某个函数(这里是USB的方法引用)作为参数“转换”了,如果Optional是empty那么就什么也不会发生。
结合使用map和filter可以检测某个声卡是否有USB 3.0的接口:
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
现在我们的代码看起来比较像是在描述问题了!而且没有任何非空检测,太酷了!
使用flatMap级联Optional
我们已经有一些常见的模式可以通过Optional重构了,那么我们如何用一种安全的方式重构下面的代码呢?
String version = computer.getSoundcard().getUSB().getVersion();
上面的代码都是从一个对象里面取出另外一个对象, 这不正是上文介绍的map吗?我们改写Computer模型对象,让它拥有一个Optional<Soundcard>
和一个Optional<USB>
,然后就可以把代码改成这样:
String version = computer.map(Computer::getSoundcard)
.map(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
但是,这段代码并不能通过编译。为什么?
computer变量类型是Optional<Computer>
,因此它调用map方法没有任何问题;但是,getSoundcard()
方法的返回类型是Optional<Soundcard>
这意味着map操作结果的类型是Optional<Optional<Soundcard>>
,因此getUsb这个调用是非法的:外面的那个Optional包含的值是另外一个Optional,自然就没有getUsb方法,下图是这个调用的结构(two level Optional):
如何解决这个问题呢?Optional类提供了一个flapMap
的方法。这个方法可以对一个Optional使用一个函数转换为一个Optional然后把结果(两个Optional)flatten为一个单个Optional,下图给出了map和flatMap的区别:
用flatMap重写我们的代码:
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");
第一个flatMap
确保返回一个Optioan<Soundcard>
而不是Optional<<Optional<Soundcard>>
,第二个flatMap
确保返回一个Optional<USB>
;接着第三次调用着需要一个map即可,因为getVersion
返回一个String
而非Optional
方法。
Cool!现在我们可以抛弃痛苦的嵌套非空检测了,使用Optional可以写出声明式的,更可读的代码,并且永远不会有空指针异常!
英文原文:
Tired of Null Pointer Exceptions? Consider Using Java SE 8's Optional!
不错,翻译的挺好。