0%

为什么要用Optional

前言

JDK8提供了Optional类,但是很多小伙伴都不知道有什么作用,感觉跟直接用null也没什么区别。那么今天咱们就谈一谈Optional有什么用,还有我为什么推荐你们使用Optional。

Optional就是用来代替null判断的吗?

我们都知道:一个Java对象可能是null,所以null判断也无处不在。我想小伙伴们可能听过这种说法:

有了Optional,以后就不需要null判断了,我们判断Optional。

这话未必正确,我们来看一个代码例子,如果有一个用null判断的代码:

1
2
3
if (maybeNullObj == null) {
log.error("对象不允许为null。");
}

有人可能会想,已经有了Optional这个工具,就别用null判断这种过时的方式了,改成下面这种多好啊。

1
2
3
if (Optional.of(maybeNullObj).isEmpty()) {
log.error("对象不允许为null。");
}

其实在上面的代码里,null判断没有问题,而下面的Optional写法完全是多余的。转换成Optional再判断空其实没有必要。

那么Optional的出现是为了什么呢?Optional难道仅仅是用来代替null判断的吗?

我们首先应该搞清楚,null有什么问题?

null有什么问题?

我先截取一段Rust编程语言官方文档里的关于null的部分。

Tony Hoare,null 的发明者,在他 2009 年的演讲 “Null References: The Billion Dollar Mistake” 中曾经说到:
我称之为我十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所有引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误、漏洞和系统崩溃,在之后的四十多年中造成了数十亿美元的苦痛和伤害。

在自己的一个方法内部使用时,null判断从来都不会有问题,出问题的是在和别人协作时。

null的最大问题就是,在协作时,缺少契约。

缺少契约体现在两个方面。对别人的承诺。和对自我的约束。

对别人的承诺

如果我们准备写一个方法给别人使用。
看一段代码:

1
2
public Object getObjectByParameter(int i) {
}

假设上面的代码是要提供给别人使用。别人在拿到我们的代码之后,他会认为这个返回值可不可以为null呢?调用之后需要对返回值做null判断吗?

大部分Java程序员都知道,这个方法可能会返回null,并且会在接返回值后进行null判断。

但是如果这个方法我们设计的非常棒,保证从来不会返回null。像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 这个方法永远不会返回null,不需要对返回值进行null判断。说谎是小狗!
*
* @param i input
* @return 再说一遍。相信我,永远永远不会返回null。
*/
@NotNull
public Object getObjectByParameter(int i) {
if (i == 0) {
System.out.println("i = " + i);
// 从数据库取得数据
return getFromDB();
}
return new Object();
}

你看,我加了方法注释,保证了我永远不会返回null。还加了一个 IntelliJ 的注解。这回,调用者还会对返回值做null判断吗?

我相信他们还会做null判断的。做null判断已经被逼成了 Java 程序员的基本素养。和返回值是 Object 比起来,我的所有的保证都脆弱不堪。因为代码里有一处逻辑还是会让返回值变为 null。

因为 null 的存在,我们无法给别人确定的承诺。

对自己的约束

别人不相信我的承诺,因为他们可能觉得我对自己的约束的不够。

在返回值是Object 的时候,永远有返回 null 的可能,因此,Java 程序员已经习惯了放任自己,不做无谓的努力,反正也没人相信一个方法永远不会返回null。

之前的这个方法,尽管我做了保证不会返回 null,但是那个getFromDB方法毁了一切保证。 我没有对他做 null 的判断。 它的返回值不是 null 的话直接返回就可以了。但如果它的返回值是 null 呢,我该怎么办呢?

说真的,我没考虑过这种情况。可能是忘了。也可能是这种情况我觉得不会出现。或者是这种情况我不知道该怎么办,是写日志还是抛出异常呢,我决定之后再考虑,但是直到最后发布版本我都没再考虑它。

1
2
3
4
5
6
7
8
public Object getObjectByParameter(int i) {
if (i == 0) {
System.out.println("i = " + i);
// 从数据库取得数据
return getFromDB();
}
return new Object();
}

所以,管它呢,我写的是 Java,每个 Java 程序员都会对返回值做null判断的,所以我决定收回我做过的一切保证,你们自己去对返回值做 null 判断玩去吧。

Optional是怎么解决问题的

Optional的出现不会改变 Object 返回值可能是 null 的事实,因为这里是 Java。但是它还是有用的。因为它明确了一种契约。

约束自己

首先要声明自己的编码契约或者规范:

  1. 可能返回空的方法都使用 Optional
  2. 不会返回空的方法直接使用对象
  3. 参数值同上

有了以上的编码契约规范,我们编码的时候就会时刻提醒自己遵守契约。

我要写一个会返回空的方法,我会这么写返回值可能为空的代码。

1
2
3
4
5
6
public Optional<Object> getMaybeEmptyObject(int i) {
if (i == 0) {
return Optional.empty();
}
return Optional.of(new Object());
}

不用再作无谓的注释说明,签名说明一切。

写一个不会返回null的方法,我会全力保证返回值不是 null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object getNeverEmptyObject(int i) throws Exception {
if (i == 0) {
// 可能为 null 的返回值
Object fromDB = getFromDB();
// 要自己做全套的判断
if (fromDB == null) {
// 出现 null 了,
log.error("不可能发生的事情发生了");
throw new Exception("我认为不会出现这种情况,出现了我也没什么办法,决定先抛出异常看看");
}
return fromDB;
}
return new Object();
}

第二点爱信不信,我做了自我约束,不信的话自己接返回值再去做 null 判断去吧。

对别人承诺

如果我对别人做了以下契约承诺。

  1. 可能返回空的方法都使用 Optional
  2. 不会返回空的方法直接使用对象
  3. 参数值同上

别人有会认为我做了自我约束和检查,他们很有可能会相信。以后他们就完全可以通过方法签名就能弄懂什么时候该是可空的,什么时候不可空。比如下面这些方法。

1
2
3
4
5
6
7
8
Optional<Object> test1(Optional<Object> input){
}
Optional<Object> test2(Object input){
}
Object test3(Object input){
}
Object test4(Optional<Object> input){
}

现在这些方法都没有方法注释了,但是我相信大部分人都知道这些方法该怎么使用了吧。

Optional的效率与null的效率差距

有的小伙伴可能会关心效率问题,咱们来做个简单的测试

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 各执行 10 亿次
private static final int MAX_COUNT = 1_000_000_000;

public static void main(String[] args) {
// 预热
int ret = 0;
for (int i = 0; i < MAX_COUNT; i++) {
Object object = getObject(i);
if (object == null) {
ret += 1;
}
}
ret = 0;
for (int i = 0; i < MAX_COUNT; i++) {
Optional<Object> optionalObject = getOptionalObject(i);
if (optionalObject.isEmpty()) {
ret += 1;
}
}

// 执行
LocalDateTime start;
LocalDateTime end;

// 测试 Optional
start = LocalDateTime.now();
ret = 0;
for (int i = 0; i < MAX_COUNT; i++) {
Optional<Object> optionalObject = getOptionalObject(i);
if (optionalObject.isEmpty()) {
ret += 1;
}
}
end = LocalDateTime.now();
long optionalCheckDuration = Duration.between(start, end).toMillis();
System.out.println("optional判断时间间隔(毫秒) = " + optionalCheckDuration);

// 测试 null
start = LocalDateTime.now();
ret = 0;
for (int i = 0; i < MAX_COUNT; i++) {
Object object = getObject(i);
if (object == null) {
ret += 1;
}
}
end = LocalDateTime.now();
long nullCheckDuration = Duration.between(start, end).toMillis();
System.out.println("null判断时间间隔(毫秒) = " + nullCheckDuration);

}

private static Object getObject(int i) {
if (i % 2 == 0) {
return null;
}
return new Object();
}

private static Optional<Object> getOptionalObject(int i) {
if (i % 2 == 0) {
return Optional.empty();
}
return Optional.of(new Object());
}
1
2
optional判断时间间隔(毫秒) = 4592
null判断时间间隔(毫秒) = 2421

可以看到在 10 亿次的测试下,Optional 的效率和 null 判断还是略有不足,毕竟多了一层的判断,但是这种级别的效率损失可以算作微乎其微了(单次相差0.000002171毫秒)

展望

如果 Java 本身不支持 null,那是种什么情况呢?如果想象不出来的话,希望小伙伴都能去学学 Rust 语言,体验一下没有 null 的编程感受。

咱们没办法改变 Java,Java 要向下兼容,未来也不会去掉 null。所以咱们只能适应它。

我希望所有的程序员都能明确自己方法的参数和返回值是否可能是空,有可能是空就用 Optional,不可能是空就直接返回对象。当所有人和所有库都这么做的话,那么null 的问题也就相当于不存在了。

总结

Optional 的存在是用来定义方法边界的,明确的指示出参数和返回值是否可空。并作为契约,同时约束着方法创建者和使用者。

欢迎关注我的其它发布渠道