1、如何为缺失的值建模
示例:
public class Person {
private Car car; public Car getCar() { return car; }
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
这段代码看起来正常,但是现实生活中很多人没有车,所以调用getCar方法的结果会抛出NullPointerException。实际中常见的做法是返回一个null引用,表示该值的缺失,即用户没有 车。但是对getInsurance的调用会返回null引用的insurance,会导致运行时出现一个NullPointerException,终止程序的运行。还有可能getInsurance的返回值也是null。
采用防御式检查减少NullPointerException
如何避免NullPointerException,通常可以在需要的地方添加null的检查,添加的方式各有不同。
//方式1-深层质疑:以嵌套判断的方式解决
public String getCarInsuranceName(Person person) {
if (person != null) {
Car car = person.getCar();
if (car != null) {
Insurance insurance = car.getInsurance();
if (insurance != null) return insurance.getName();
}
}
return "unknown";
}
这个方法每次引用一个变量都会做一次null检查,如果引用链上的任何一个遍历的变量值为null,它就返回一个值为“unknown”的字符串。
深层质疑:每次你不确定一 个变量是否为null时,都需要添加一个进一步嵌套的if块,也增加了代码缩进的层数。这种方式不具备扩展性,同时还牺牲了代码的可读性。
//方式2-过多的退出语句
public String getCarInsuranceName(Person person) {
if (person == null) return "unknown";
Car car = person.getCar();
if (car == null) return "unknown";
Insurance insurance = car.getInsurance();
if (insurance == null) return "unknown";
return insurance.getName();
}
为了避免深层递归的if语句块,采用了一种不同的策略:每次你遭遇null变量,都返回一个字符串常量“unknown”。这种方案远非理想,这个方法有了四个不同的退出点,使得代码的维护异常艰难,发生null时返回的默认值,即字符串“unknown”在三个不同的地方重复出现。这种流程是极易出错的;
null带来的问题
1)它是错误之源,NullPointerException是目前Java程序开发中典型的异常。
2)它会使你的代码膨胀,让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕。
3)它自身是毫无意义的,null自身没有任何的语义,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。
4)它破坏了Java的哲学,Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。
5)它在Java的类型系统上开了个口子,null并不属于任何类型,它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,将无法获知这个null变量初的赋值到底是什么类型。
其他语言中null的替代品
Groovy语言通过引入安全导航操作符(Safe Navigation Operator,标记为?)可以安全访问可能为null的变量。
def carInsuranceName = person?.car?.insurance?.name
person对象可能没有car对象,你试图通过赋一个null给Person对象的car引用,对这种可能性建模。类似地,car也可能没有insurance。Groovy的安全导航操作符能够避免在访问这些可能为null引用的变量时抛出NullPointerException,在调用链中的变量遭遇null时将null引用沿着调用链传递下去,返回一个null。
Haskell语言中包含 了一个Maybe类型,它本质上是对optional值的封装。Maybe类型的变量可以是指定类型的值,也可以什么都不是。它并没有null引用的概念。
Scala语言有类似的数据结构,名字叫Option[T], 它既可以包含类型为T的变量,也可以不包含该变量。要使用这种类型,必须显式地调用Option类型的available操作,检查该变量是否有值,这其实也是一种变相的“null检查”。
2、Optional类入门
Java 8从“optional值”中吸取了灵感,引入了一个名为 java.util.Optional<T>的新类。这是一个封装Optional值的类。
使用Optional定义的Car类
变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由Optional.empty()返回。Optional.empty()是一个静态工厂方法,它返回Optional类的特定单一实例。
//使用Optional重新定义Person/Car/Insurance的数据模型
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
person引用的是Optional<Car>,而car引用的是Optional<Insurance>,这种方式非常清晰地表达了模型中一个person可能拥有也可能没有car的情形,car可能进行了保险,也可能没有保险。
使用Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是算法上的缺陷,或是数据中的问题。引入Optional 类的意图并非要消除每一个null引用,它的目标是帮助你更好地设计出普适的API,让程序员看到方法签名,就能了解它是否接受一个Optional的值。
--参考文献《Java8实战》







网友评论