Java 8加入的Optional在实际使用时是如何让人红温的

如果写过一段时间的Java代码,相信对NullPointerException不会陌生。试想一个这样的场景,现在有一个这样的接口DemoInterface

interface DemoInterface {
  TargetObject getTargetObject();
}

通过某些方式,你获得了一个DemoInterface的实现对象:

DemoInterface demoObj = DemoFactory.get(args);

现在你需要拿到TargetObject对象,调用它的某些方法,对它做些什么。于是我们自然地写了一个这样的代码:

demoObject.doSomething();

然后你一运行,结果发现报错了。

Exception in thread "main" java.lang.NullPointerException
      at DemoProgram.main(DemoProgram.java:23333)

仔细一看......喔,原来是因为我们太相信getTargetObject方法的返回值了,没有考虑到它有可能返回一个null

发明null的Tony Hoare在自己的书中曾经提到,null是一个价值十亿美元的错误。Java作为一个引入了空引用null概念的语言,在做引用的访问操作前,往往需要对引用检查是否为null,来判断是否有效。在实际使用时,null就像地雷,稍不留意就踩了上面,直接就炸了。

一些新的现代化语言通过各种方式避免了这样的问题。比如在Kotlin中就有一套成熟的空值处理机制。Java作为一套有历史包袱的语言,对于null问题显然不能采取这样激进的策略。

于是在Java 8里,引入了Optional

什么是Optional

实际上Java 8已经推出很久了,Optional也是老朋友了。如果有一个方法可能会产生空值null,更优秀的设计是让他返回一个Optional对象,用来表示这个方法有可能不会返回一个调用者期待的实际结果,有可能是空的,以此来强迫调用者注意考虑这种情况。

在刚才的例子中,设计DemoInterface的人或许应该这样做:

interface DemoInterface {
  Optional<TargetObject> getTargetObject();
}

对于实现getTargetObject的人,方法在返回值的时候,可以简单使用Optional.ofNullable完成返回值包装:

class DemoObject implements DemoInterface {
    public Optional<TargetObject> getTargetObject() {
        TargetObject targetObj = somethingGetTargetObject();
        return Optional.ofNullable(targetObject);
    }
}

可以发现,这样做也没什么费劲的,原来是直接返回targetObject,现在只是在外面套一层Optional.ofNullable就行了,很方便。原有的接口逻辑迁移到Optional上的话,体验还是很丝滑的,没什么障碍。

对于使用者的话,那就需要做一些判断逻辑了。这取决于这里的业务打算怎样做。比如有两种情况:

第一种情况,调用者需要必须拿到值,否则下面的逻辑一点都走不通。那可以使用isPresent来判断这个Optional表示的对象到底存不存在,就像这样:

Optional<TargetObject> targetObjOptional = demoObj.getTargetObject();
if (targetObjOptional.isPresent()) {
  TargetObject targetObj = targetObjOptional.get(); //实际表示的值,一定存在
  //do something...
}

第二种情况,调用者在没拿到值的时候,可以有个默认值,那用orElse就很方便。就像这样:

// 这里的orElse表示:
//   - 如果对应的targetObj存在,那就是那个值
//   - 如果不存在,那就是取一个默认的新new的TargetObject对象
TargetObject targetObj = demoObj.getTargetObject().orElse(new TargetObject());
//do something...

当然,刚才的用法只是一个简单的演示,实际上的用法有很多。例如,其实这里用orElse其实并不完美,如果只是String这种开销比较小的对象这样用是可以的,但是我们这里对于一个自定义的TargetObject也这样用,会使得每次调用时,哪怕对应的targetObj存在也都会额外new一个新的TargetObject,因此可以用更合适的orElseGet替代。再者,这里为了举例方便,第一个例子里直接使用isPresent做if检查的做法也不太妥,这使得检查跟Java 8之前的做法没什么区别,其实isPresent一般用于流处理的结尾,用来判断是否符合条件更合适。

Optional在实际使用中是如何让人红温的

说完了Optional是什么,不难发现这玩意儿是存在它的必要性的。正如一开始我们提到的那样,Optional用来做可能会返回null的方法的返回值包装具有很大的优越性,可以让代码更优雅。

那这玩意儿在实际使用中到底是怎么让人红温的呢?

当Optional遇到传统与创新结合的老哥

首先出场的是传统与创新结合的老哥,它给自己的方法用上了Optional,但是仍然忘记不了心心念念的null。有一天,他给大伙提供了一个这样的接口:

interface FuckingInterface {
    Optional<FuckingTargetObject> getObj();
}

你一看,我草这个接口好啊,它居然用了先进的Optional。于是你满心欢喜的开始调用:

FuckingInterface fuck = ...;
FuckingTargetObject tar = fuck.getObj().orElseGet(() -> new FuckingTargetObject());
tar.doSomething();

然后......代码报错了!是什么报错呢,你仔细一看...嗯?NullPointerException

如果你是一个对世界抱有真善美幻想的萌新,那你很容易会陷入这样的误导,觉得Optional是用来解决null问题的,那你如果看报错不够仔细,没有看清楚行号,第一反应有可能是觉得后面的调用逻辑有的问题,于是你去看了半天tar.doSomething()里的逻辑问题......

然后发现看了半天没什么头绪,于是你仔细看了报错,诶?报错的行号为什么是拿tar对象的这一行?这一行不是用了Optional吗?后面的orElseGet里面也不可能拿到空的FuckingTargetObject,因为这里是直接new的,怎么可能null的?

经过仔细的思考你终于反应过来了,你遇到了传统与创新结合的老哥。方法返回Optional 不等于 方法不能返回null,用不等式做题就是快。你搞了半天,原来是这个啥比getObj返回了个nullOptional<FuckingTargetObject>对象。

于是在实际生产中,为了防止同事小伙伴给你来这一出把你送走了,你不得不还得对这个Optional<FuckingTargetObject>做判null逻辑,我们的逻辑就改成了:

FuckingInterface fuck = ...;
Optional<FuckingTargetObject> tarOptional = fuck.getObj();
FuckingTargetObject tar = null;
if (tarOptional != null) {
    tar = tarOptional.orElseGet(() -> new FuckingTargetObject());
} else {
    tar = new FuckingTargetObject();
}
tar.doSomething();

这直接就把Optional的优雅给破坏了,感谢这位老哥,让后面调用他接口的人写的代码都跟着带有了一种传统与创新结合的美。我们对这个接口的使用彻底退化成了对Optional的拆包逻辑,使得本来应该发挥它优势的Optional变得十分的没必要,用了Optional照样还得写一个完全没必要的判null,反而还使得代码平白无故多了个封装成Optional和拆开Optional的开销,俨然成为了Java界的人类迷惑行为。

当Optional遇到思维开放老哥

第二位登场的老哥的思维更开放一些,他认为我们没必要把眼光局限在接口的返回值这么狭小的场景里,或许方法的参数也可以用上Optional,于是他写了这样的逻辑:

void dealWithTargetObject(Optional<TargetObject> targetObjOptional, Optional<Integer> countOptional) {
    TargetObject targetObj = targetObjOptional.orElseThrow(
      () -> new IllegalArgumentException("TargetObject is neccessary!")
    );
    Integer count = countOptional.orElseThrow(
      () -> new IllegalArgumentException("Count is neccessary!")
    );
    // do something...
}

实际上,如果把Optional作为方法的参数传递,IDEA是会给warning的。不过IDEA并没有给清楚这个warning不让这样做的原因是什么。其实这个问题的答案其实也不难想到。

首先,我们为什么要给接口的返回值返回Optional呢,是因为我们希望使用者能够注意到这里可能存在潜在的空值,后续的逻辑需要根据Optional做对应的空值判断逻辑。
注意到这里的逻辑关系,其实Optional本质是一种,你作为“前辈”,对你这段逻辑的“后人”的约束手段。
整明白了这件事,那就好解释了。你给方法的参数搞这个图啥?你这个方法的调用者是“前人”,你是这个参数的“后人”,人家上游给你丢啥是你能控制的了的?他给你丢个null咋办,你是不是还得在方法里判断Optional对象是不是null,来确保参数的合法性?
明白了这个道理就不难发现,那你还不如第一个让人红温的传统与创新结合的老哥,最起码他的啥比是让后人红温,你这样做是让调用者多套了一层Optional带来了不便,你还会因为需要验证参数合法性给自己整红温了,你这样干就是纯纯小丑。

不过这里提到的这一点更多是针对于一个public的方法。如果这个方法是一个类里内部的小工具方法,是private的,自己内部处理用的,那其实我感觉也没啥的。

当Optional遇到思维混乱老哥

思维混乱老哥秉承站在思维开放老哥的肩膀上睁眼看世界的精神,他更激进,打算给类的字段也用上Optional

@Data
public class FuckingUser {
    private Optional<String> username;
    private Optional<Int> age;
}

此事在jdk maillist中亦有记载,参见Shouldn't Optional be Serializable?。根据官方的描述,这玩意儿就是给返回值设计的。如果它能作为类的字段处理,那它就不得不需要被考虑持久化的问题了。没有这个能力知道吧.jpg(

基于这样的考虑,Optional也应该避免出现在泛型约束里,此事在jdk maillist里亦有记载,参见Loose ends: Optional。写一个类似List<Optional<String>>这样的东西就很难评。

并且针对类字段用Optional的情况,如果全用上Optional仔细想想也很难评。你这就显得像是告诉别人,你千万别相信我代码,每个地方都有可能null喔......有一种沾点大病的美。

总结

综上所述,Java 8引入了Optional能使得判空逻辑更为优雅,使用时绝大多数情况下应该尽可能应用在方法的返回值上。

上一篇