Source:Tired of Null Pointer Exceptions? Consider Using Java SE 8’s Optional!
Optional1
public final class Optional<T> extends Object
写 Java 程序很容易出现空指针异常,每次使用值之前都要做一次判空也是让人不胜其烦。听说 Optional 可以减轻这个负担,于是有了这篇文章来记录下。第一次见到 Optional 的概念是在 Scala,不过只是一带而过,没有太多的了解。趁着最近有时间好好看看。
从 oracle 拷过来的例子:
如果理想世界存在的话,我们的代码是下面这个样子的:
1 | String version = computer.getSoundcard().getUSB().getVersion(); |
怎么可能会有这种好事呢,对吧。这中间computer
,getSoundcard()
,getUSB()
都有可能返回null
,一环出错程序必然 GG。在以前,为了程序正常运行我们大概会把代码写成下面这样:
1 | String version = "UNKNOWN"; |
这才对嘛,呵呵哒!上面的代码不光写起来繁琐,更容易出错,顺便还降低了代码的可读性。真是罪大恶极。这个锅谁来背一下?
其他语言的经验
再看Java的新解决办法前,先了解下别人家的糖是什么样的。
Java 的后辈 Groovy 中使用?.
解决这个问题。写起来大概这样:
1 | String version = computer?.getSoundcard()?.getUSB()?.getVersion(); |
另外还有?:
这个符号可以做默认值的设置:
1 | String version = computer?.getSoundcard()?.getUSB()?.getVersion()?:"UNKNOWN"; |
挺好用哈!还有其他函数式语言 Haskell 和 Scala 又分别采取了各不相同的方法。Haskell 使用一个Maybe
类型,它可以代表给定类型的值也可以什么都不是,没有空引用的概念。Scala 也有一个和这个很像的东西Option[T]
,你需要自己显示的检查这个值到底只是空还是非空。这样你就不会漏掉任何一个判断了。
Optional 简介
回到 Java 8,Java 8 引入了一个新的工具类java.util.Optional<T>
,灵感来自 Haskell 和 Scala。你可以把它看作一个单一值的容器,里边可能有对象,也有可能是空的。
使用 Optional 更新上面的例子
1 | public class Computer { |
上面的代码表示的含义是有个电脑,它可能有声卡也可能没有,如果有声卡的话,可能有一个 USB 端口(好绕)。
使用Optional
的好处就是强迫你去思考如果返回为null
时的应对策略。需要注意不是所有返回值都要用Optional
包装,它的本意是帮你构建更易于理解的 API,从方法的签名就可以预知你需要处理一个Optional
值,再也不会落下。
Optional 的使用模式
介绍已经讲完,来看看代码吧!
先 show 一下完全重写后的代码。有了 Java 8,我们可以采用新的方法。
1 | String name = computer.flatMap(Computer::getSoundcard) |
有关 Java 8 lambdas 的内容可以在这查看。
如何创建一个 Optional 对象
1 | // 创建一个空的 Optional |
对 Optional 做点啥操作
使用 Optional 就不用像以前那样检查为不为null
了。
1 | // 可以使用`isPresent()`这个方法。(不推荐) |
Default Values and Actions
Optional 的典型应用场景就是判断一个值为空时执行一个操作。1
2
3
4
5
6
7
8
9// 过去我们用三元表达式这么写
Soundcard soundcard =
maybeSoundcard != null ? maybeSoundcard
: new Soundcard("basic_sound_card");
// 时代在进步,兄弟!
// 当 Optional maybeSoundcard 为空时会执行 orElse() 方法
Soundcard soundcard = maybeSoundcard.orElse(new Soundcard("defaut"));
// 再或者直接抛出异常
Soundcard soundcard = maybeSoundCard.orElseThrow(IllegalStateException::new);
使用 filter 过滤一些 values
某些场景下,你可能需要判断某个条件选择执行还是不执行一个方法。
像这样:1
2
3
4
5
6
7
8USB usb = ...;
if(usb != null && "3.0".equals(usb.getVersion())){
System.out.println("ok");
}
// 采用 Optional 模式重写后
Optional<USB> maybeUSB = ...;
maybeUSB.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));
Extracting and Transforming Values Using the map Method
使用map
操作符提取转换 values。
1 | if(soundcard != null){ |
使用 flatMap 进行链式调用
我们已经介绍了一些操作符,下面就来重写这段代码:1
String version = computer.getSoundcard().getUSB().getVersion();
看了上面的介绍,你可能直接就写出了这段代码:1
2
3
4String 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()
时会出现问题。
flatMap
操作符就是用来处理这种场景的。flatMap会对stream中所有元素执行参数func,并将所有返回值做合成一个新的stream返回。
Optional 当然也支持flatMap
操作。
我们的最终代码到这就可以完成了!1
2
3
4
5String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
// USB::getVersion 返回值是 String,所以使用 map
.map(USB::getVersion)
.orElse("UNKNOWN");
我对flatMap
的理解还不是很深,不能很好地描述出它的特性,下面一段代码可能会有所帮助。
1 | public static void main(String[] args) { |
运行结果1
2
3
4
5
6
7
8// res1
10 11 12 13 20 21 22 23
// res2
java.util.stream.ReferencePipeline$Head@b4c966a
10 11 12 13
java.util.stream.ReferencePipeline$Head@4e50df2e
20 21 22 23
可以看到,flatMap
操作将接收到的 List 拆开,再将所有元素重新组合成一个Stream,而map
是没有这步处理的。
结语
没啥好说的,快用 Java8!