2016-09-06 | learn

Java Optional

Source:Tired of Null Pointer Exceptions? Consider Using Java SE 8’s Optional!

Optional

1
public final class Optional<T> extends Object

写 Java 程序很容易出现空指针异常,每次使用值之前都要做一次判空也是让人不胜其烦。听说 Optional 可以减轻这个负担,于是有了这篇文章来记录下。第一次见到 Optional 的概念是在 Scala,不过只是一带而过,没有太多的了解。趁着最近有时间好好看看。

从 oracle 拷过来的例子:
A nested structure for representing a Computer

如果理想世界存在的话,我们的代码是下面这个样子的:

1
String version = computer.getSoundcard().getUSB().getVersion();

怎么可能会有这种好事呢,对吧。这中间computergetSoundcard()getUSB() 都有可能返回null,一环出错程序必然 GG。在以前,为了程序正常运行我们大概会把代码写成下面这样:

1
2
3
4
5
6
7
8
9
10
String version = "UNKNOWN";
if(computer != null){
Soundcard soundcard = computer.getSoundcard();
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null){
version = usb.getVersion();
}
}
}

这才对嘛,呵呵哒!上面的代码不光写起来繁琐,更容易出错,顺便还降低了代码的可读性。真是罪大恶极。这个锅谁来背一下?

其他语言的经验

再看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。你可以把它看作一个单一值的容器,里边可能有对象,也有可能是空的。

An optional sound card

使用 Optional 更新上面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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(){ ... }
}

上面的代码表示的含义是有个电脑,它可能有声卡也可能没有,如果有声卡的话,可能有一个 USB 端口(好绕)。
使用Optional的好处就是强迫你去思考如果返回为null时的应对策略。需要注意不是所有返回值都要用Optional包装,它的本意是帮你构建更易于理解的 API,从方法的签名就可以预知你需要处理一个Optional值,再也不会落下。

Optional 的使用模式

介绍已经讲完,来看看代码吧!
先 show 一下完全重写后的代码。有了 Java 8,我们可以采用新的方法。

1
2
3
4
String name = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
.map(USB::getVersion)
.orElse("UNKNOWN");

有关 Java 8 lambdas 的内容可以在这查看。

如何创建一个 Optional 对象

1
2
3
4
5
6
7
8
9
10
// 创建一个空的 Optional
Optional<Soundcard> sc = Optional.empty();

// And here is an Optional with a non-null value:
// 如果传入的 soundcard == null 会立即抛出空指针异常,不会等到你调用时才抛
SoundCard soundcard = new Soundcard();
Optional<Soundcard> sc = Optional.of(soundcard);

// 还可以这么创建
Optional<Soundcard> sc = Optional.ofNullable(soundcard);

对 Optional 做点啥操作

使用 Optional 就不用像以前那样检查为不为null了。

1
2
3
4
5
6
7
8
// 可以使用`isPresent()`这个方法。(不推荐)
if(soundcard.isPresent()){
System.out.println(soundcard.get());
}
// 也可以这样直接用,方便多了(推荐)
Optional<Soundcard> soundcard = ...;
soundcard.ifPresent(System.out::println);

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
8
USB 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
2
3
4
5
6
7
8
9
10
11
if(soundcard != null){
USB usb = soundcard.getUSB();
if(usb != null && "3.0".equals(usb.getVersion()){
System.out.println("ok");
}
}

// 我们的代码看起来越来越简洁了!
maybeSoundcard.map(Soundcard::getUSB)
.filter(usb -> "3.0".equals(usb.getVersion())
.ifPresent(() -> System.out.println("ok"));

使用 flatMap 进行链式调用

我们已经介绍了一些操作符,下面就来重写这段代码:

1
String version = computer.getSoundcard().getUSB().getVersion();

看了上面的介绍,你可能直接就写出了这段代码:

1
2
3
4
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()时会出现问题。

A two-level Optional

flatMap操作符就是用来处理这种场景的。flatMap会对stream中所有元素执行参数func,并将所有返回值做合成一个新的stream返回。
Optional 当然也支持flatMap操作。

Using map versus flatMap with Optional

我们的最终代码到这就可以完成了!

1
2
3
4
5
String version = computer.flatMap(Computer::getSoundcard)
.flatMap(Soundcard::getUSB)
// USB::getVersion 返回值是 String,所以使用 map
.map(USB::getVersion)
.orElse("UNKNOWN");

我对flatMap的理解还不是很深,不能很好地描述出它的特性,下面一段代码可能会有所帮助。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
List<List<Integer>> listContainer = new ArrayList<>();

list1.add(10);
list1.add(11);
list1.add(12);
list1.add(13);

list2.add(20);
list2.add(21);
list2.add(22);
list2.add(23);

listContainer.add(list1);
listContainer.add(list2);

List res1 = listContainer.stream()
.flatMap(List::stream)
.collect(Collectors.toList());

List res2 = listContainer.stream()
.map(List::stream)
.collect(Collectors.toList());


res1.forEach(var -> {
System.out.print(var + " ");
});

res2.forEach(var -> {
System.out.println();
System.out.print(var);
if (var instanceof Stream) {
System.out.println();
((Stream) var).forEach(item -> {
System.out.print(item + " ");
});
}
});
}

运行结果

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!