如何为缺失的值建模
假设需要处理下面的这样的一个对象,这是一个拥有汽车汽车保险的客户。
客户类
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 void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}
这是一段很平常的代码,但是现实生活中很多人没有汽车,所以调用getCar()方法会怎么样呢?一种常见的做法是返回一个NULL引用,表示该值的缺失,即用户没有车。而接下来,对getInsurance的调用会返回null引用的insurance,这导致运行时出现了NullPointerException,终止程序的运行。
采用防御式检查减少NullPointerException
怎么样才能避免空指针异常呢?通常的做法,是对可能为空的对象做非空验证 ,下面例子将试图避免空指针异常。
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判断语句,这种方式不具备扩展性,同时还牺牲了代码的可读性。面对这种尴尬的情况,我们看看下面一种方案。
public String getCarInsuranceName1(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语句块,但是这种代码仍然不是很理想,首先,方法有4个退出点,增加维护成本,同时还可能因为忘记对某个对象判空导致异常。
Optional 类入门
Java 8 中引入了一个新的类java.util.Optional<T> ,这个是有个封装Optional值的类。使用这个类,表示一个人可能有车也可能没有车,变量存在时,Optional只是对Car类简单封装,变量不存在时,缺失的值会被构建成一个空的Optional对象,由方法Optional.empty()返回。那返回null和返回Optional.empty()有什么区别呢?如果尝试解引用null,会触发空指针异常,但是使用Optional.empty()则不会。
下面使用Optional类对之前代码重构。
客户类
public class Person {
private Optional<Car> car;
public void setCar(Optional<Car> car){
this.car = car;
}
}
汽车类
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance(){
return insurance;
}
}
保险类
public class Insurance {
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}
现在可以看到Optional丰富了模型的语义,通过代码,就能知道哪些对象可能为空,在代码中使用Optional能非常清晰的界定出变量值的缺失是结构上的问题还是数据中的问题,另外,引用Optional并不是为了消除每一个null 引用,它的目标是帮助开发者更好设计出普适的API,直面缺失的变量值。
应用Optional的几种模式
创建Optional对象
声明一个空的Optional对象
Optional opt = Optional.empty();
创建一个非空的Optional对象
Car car = new Car();
Optional<Car> optCar = Optional.of(car); //如果car未初始化,此处将会抛异常
可接受null值的Optional对象
Car car = null;
Optional<Car> optCar = Optional.ofNullable(car);
使用map从Optional对象中提取和转换值
从对象中提取信息是一种比较常见的场景。比如,你可能想要冲insurance公司对象中提取公司的名字,代码如下:
String name = null;
if(insurance != null){
name = insurance.getName();
}
为了支持这种模式,Optional提供了一个map方法,它的工作方式如下:
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
map操作会将提供的函数应用到流中个每一个元素,如果Optional包含一个值,那函数就将该值作为参数传递给map,对改值进行转换。如果Optional为空,那么就是什么都不做。
使用 flatMap 链接 Optional 对象
刚刚学了map方法,那么我们可能想这样重构之前的代码
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getCar).map(Car::getInsurance).map(Insurance::getName);
但是,这段代码并不能通过编译,原因是getCar()返回的是Optional<Car>对象,这意味着第二个map操作的是一个Optional<Optional<car>>对象,这就遇到了套嵌式Optional结构。那么解决方法就是flatMap方法,这个方法接受一个函数作为参数,这个函数的返回值是另一个流。这个方法会应用到流中的每一个元素,最终形成一个新的流的流。但是flatMap会用流的内容替换每一个新生产的流。那么再次对之前的代码重构,如下。
public String getCarInsuranceName(Optional<Car> person){
return person.flatMap(Person::getName).flatMap(Car::getInsurance).map(Insurance::getName).orElse("Unknown");
}
默认行为及解引用Optional对象
Optional类提供了多种方法读取Optional实例中的变量值。
get 这个方法简单但是不安全,如果变量存在这返回变量值,否则就抛出一个异常。
orElse 这个方法在Optional不存在值时提供一个默认值。
orElseGet orElse方法升级版,如果创建默认对象开销比较大,可以使用此方法。
orElseThrow 同样在Optional为空时抛出异常,但是异常可以自定义。
ifPresent 当变量存在时执行一个传入的方法,否则不操作。
使用 filter 剔除 特定的值
业务中可能经常会有这样的需求,检查保险公司名称是否为“Cam-Insurance”,或许我们都习惯这样做:
Insurance insurance = ...;
if(insurance != null && "Cam-Insurance".equal(insurance.getName())){
System.out.println("ok");
}
使用filter方法重构下:
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "Cam-Insurance".equal(insurance.getName())).ifPresent(x -> System.out.println("ok"));
总结
null 引用在历史上被引入到程序设计语言当中,目的是为了表示变量值的缺失。
Java 8 引入Optional类对存在或缺失的变量值进行建模。
可以使用 empty(空对象),of(不能为空),ofNullable(允许为空)来创建Optional对象。
Optional支持多种方法,如map,flatMap,filter。
使用Optional 会迫使你更积极的解引用Optional对象,以应对变量值缺失的问题,最终,你能更有效的只是代码中出现空指针异常。
使用Optional 能增加Api的可读性。