在 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 。
有两个解决方案
- 改变设计,如果“操作”和 IO 相关,那么失败的时候,不是返回 null ,而是抛出自定异常。
- 如果“操作”和 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
有类似的操作。