在 Java 中,我们尽量避免使用 null

null 在 java 里面是一个十分特殊的值,他可以是任何类型。我们应该尽量避免使用这个值,主要原因是当你使用任何 method 的时候,都会导致 NPE (NullPointerException)。

例如,

if(xxx.equals("hello")) {
    ...
}

这里 xxx 有可能是 null ,会导致 NPE ,所以有的时候,我们按照下面的方式写。

if("hello".equals(xxx)) {
    ...
}

但是如果比较 xxx, yyy 就麻烦了,

if(xxx.equals(yyy)) {
    ...
}

如果 xxx 是 null, 也会导致 NPE ,于是我们按照下面的方式写

if(xxx!=null && xxx.equals(yyy)){
    ...
}

但是,如果 yyy 也是 null ,这个判断结果是 false 。这个根据业务逻辑,某些情况下合理,某些情况下不合理。

甚至下面的代码都会可能产生 NPE

Boolean flag = getFlag();
if(flag) {
...
}

如果 flag 是 null ,就崩了。 Boolean 本来建模的是一个类型,类型中是一个集合,里面有两个元素,True 和 False ,可惜 Java 语言中,实际上是三值元素的集合,因为还有 null 这个万能值。这个严重影响了语义表达,影响建模。

如此基本的操作都会导致这么麻烦的代码和逻辑,那么,我们不如约定,所有的引用值,都不允许是 null 。某些注明的第三方库已经应用这种模式,例如 rxjava 2.0 。 rxjava 1.0 允许流中可以观察到 null 的值,而 rxjava 2.0 对此作出改进, 规定流中如果观察到了 null ,库直接抛 NPE 。 Guava 里面的某些类的设计,也是基于此原则。

如果有了这样一个约定,“所有引用值都不是 null ”,以上问题都解决了。也就是说,如果出现 NPE ,那么就以为这严重的逻辑 bug 。

有人想,如果我就想建模“可选”这个概念,如果某个操作成功,返回有效引用,如果失败,返回 null 。

有两个解决方案

  1. 改变设计,如果“操作”和 IO 相关,那么失败的时候,不是返回 null ,而是抛出自定异常。
  2. 如果“操作”和 IO 无关,不适合抛出自定义异常,那么使用 Optional 。

Java 8 里面提供了 java.util.Optional 这个类,显式建模这个“失败”的概念。

Integer parseInterger(String x){
   return Integer.valueOf(x)
}

上面的代码采用了第一种方案,如果转换整数失败,那么抛出 java.lang.NumberFormatException 的异常。

如果调用者不想处理异常,我们可以改成第二种方案。

Optional<Integer> parseInteger(String x) {
    try {
       return Optional.of(Integer.valueOf(x));
    }catch (NumberFormatException e){
       return Optional.empty();
    }
}

这个时候,调用者就需要

final Optional<Integer> aInt = parseInteger("123");
if(aInt.isPresent()){
    doSomething(aInt.get());
} else {
    doSomething(0); // assume default value is 0.
}

这么写显得很笨重,如果 Optional 多嵌套基层,代码就很难看了。

Optional<Double> aDouble;
if(aInt.isPresent()){
    aDouble = doSomethingWithInt(aInt.get());
}else {
    aDouble = doSomethingWithInt(0);
}
if(aDouble.isPresent()){
    doSomethingWithDouble(aDouble.get());
}else {
    doSomethingWithDouble(0.0);
}

我们可以改进一下

 Optional<Double> aDouble;
 Optional<Double> aDouble = doSomethingWithInt(aInt.orElse(0));
 doSomethingWithDouble(aDouble.orElse(0.0));

也就是说,orElse 提供了缺省值的模式。

如果说缺省值模式,不适合,例如,业务逻辑的需求是如果解析成功,做某事,否则什么都不做。

if(aInt.isPresent()){
    Optional<Double> aDouble = doSomethingWithInt(aInt.get(0));
    if(aDouble.isPresent()) {
        doSomethingWithDouble(aDouble.get());
    }
}

可以想象,每判断一次,程序向右缩进一次,代码也很难看,我们可以改进一下。

aInt.ifPresent(aIntegerValue -> {
    Optional<Double> aDouble = doSomethingWithInt(aIntegerValue);
    return aDouble.ifPresent(aDoubleValue ->{
        doSomethingWithDouble(aDoubleValue);
    });
});

我觉得这个很难称得上改进,但是 ifPresent 的确有应用的场景,我们换一个方式。

aInt.flatMap(YourClass::doSomethingWithInt)
    .flatMap(YourClass::doSomethingWithDouble)
    .orElse(aDefaultValue)

注: flatMap 就是 haskell 里面 bind 操作符, Optional 对应的就是 Maybe Monad 。

flatMap 很好的使用了这种模式。 map 有类似的操作。