Java8:Optional - 解救分支地狱

相信任何一个Java开发者都会遇到NPE(即NullPointerException),而为了避免出现NPE,通常的方法是在访问对象前进行判空,即使用分支语句进行判断if (xx != null)。但是,过多的分支语句(尤其是和业务逻辑结合起来后),会导致代码可读性和可维护性的下降,因此需要减少它的出现。还好Java8提供了Optional这个工具,能够消除大部分判空和一部分普通判断。

Optional

Optional<T> 代表一种有或为空的数据,使用Optional.ofNullable()可以创建一个接受null的Optional,而使用Optional.of()创建Optional时,传入参数为null就会立即抛出NPE。本文并非介绍Optional中各个方法的使用,而是在具体情形下的Optional取代if的实例。

替代判空分支

绝大部分的判空分支可以被Optional替代。

Object sth = ...;
// 为空抛出业务异常
if(sth == null){
    throw new Exception...
}
// 或者提供默认值
if(sth == null){
    sth = ...
}
// 从另外一个来源获取
if(sth == null){
    sth = getFrom...()
}
// 不为空时,干点别的
if(sth != null){
    ...
}
// 不为空就,为空则(Java 9 以上适用)
if(sth != null){
    ...
}else{
    ...
}

均可使用Optional的对应方法重构:

// 为空抛出业务异常
Optional.ofNullable(...).orElseThrow(() -> new Exception...);
// 或者提供默认值
Optional.ofNullable(...).orElse(xxx);
// 从另外一个来源获取
Optional.ofNullable(...).orElseGet(() -> getFrom...());
// 不为空时,干点别的
Optional.ofNullable(...).ifPresent(x -> ...);
// 不为空就,为空则(Java 9 以上适用)
Optional.ofNullable(...).ifPresentOrElse(x -> ...,() -> ...);

安全链式调用

Object result = sth.getA().getB().getC().getD().getE();

链式调用写起来确实爽,可是用着的时候其中如果有一个是null,那就要抓狂了😫,而且由于null的字段可能不确定,因此可能需要嵌套分支或者使用一个超长条件分支判空。

if(sth.getA() != null){
    Object sth2 = sth.getA();
    if(sth2.getB() != null){
        ...
    }
}
// 或者
if(sth.getA() != null && sth.getA().getB() != null ...){
    return ...
}
// 其他的不举例了

使用Optional的map方法可以避免这一点:

Optional.ofNullable(sth).map(xxx::getA).map(yyy::getB)...

map会自动对返回值包装成Optional,与map相对的还有flatMap方法,它只接受返回为Optional的方法引用。引入flatMap是为了防止出现多重包装,例如Optional<Optional>。

public class X{
    public Optional<Y> getName(){
        return ...
    }
}
X sth = ...
Optional.ofNullable(sth).flatMap(X::getName)...

替代部分条件判断分支

Optional也能替代一些简单的Bean条件判断,利用的是filter这个方法。

XXX sth = ...
if(sth.getB() > 10){
    return sth.getB().getC();
}
if(sth.getA() > 5){
    throw new ...
}

// 使用Optional进行替代
// 注意这里这样子的话getC()的返回值就不能是null了,否则会抛出NoSuchElementException,要不把返回值类型改为Optional<T>?
return Optional.of(sth).map(XXX::getB).filter(x -> x > 10).map(XXX:getB).get();
// 注意这里是orElse,所以filter要反过来!
Optional.of(sth).map(XXX::getA).filter(x -> x <= 5).orElseThrow(() -> new ...);

使用if的优秀实践

即使是必须使用if的情况,也存在着一些技巧可以优化代码的结构。一个原则是:尽量减少代码嵌套,代码短的分支路径写在if下,必要时反转if。
一种思想是将if作为判断特殊条件的过滤器,及时判断「异常状态」并返回,以登录为例:

if(checkPassword(password,userInput)){
    // 登录验证密码成功后可能会进行一堆操作
    ...
}else{
    // 如果上面的代码非常多,不回顾下不知道是什么的else
    throw new Exception("密码错误")
}
// 但如果是下面的写法,可能会好多了
if(!checkPassword(password,userInput)){
    // 代码短的分支路径写在if下
    // 调转if的条件,把验证密码错误作为一个特例进行过滤
    throw new Exception("密码错误")
}
// 登录验证密码成功后的一堆操作,就不在分支里面,当作正常处理
...

文章参考文献

Java8 实战

Java Optional API

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。