用Optional取代null

124 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情

NullPointerException对于任何一位Java程序员来说可能都遇到过,一切的源头就是null引用。因为null引用的存在,程序员对对象字段进行检查,判断它的值是否为期望的格式,最终却发现我们查看的并不是一个对象,而是一个空指针,它会立即抛出一个让人厌烦的NullPointerException异常。

场景一:如何为缺失的值建模

Person/Car/Insusrance的数据模型

public class Person {
    private Car car;
    private int age;

    public Car getCar() {
        return car;
    }

    public int getAge() {
        return age;
    }
}

public class Car {
    private Insurance insurance;

    public Insurance getInsurance() {
        return insurance;
    }
}

public class Insurance {
    private String name;

    public Insurance() {
    }
    public Insurance(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

我们实现这么一个方法,返回买车人的保险公司

public String getCarInsuranceName(Person person) {
  return person.getCar().getInsurance().getName();
}

null-安全的第一种尝试:深层质疑

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-安全的第二种尝试:过多的退出语句

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();
}

每次遇到null变量,都返回一个字符串常量"Unknown",这种方案远非理想,这个方法有四个不同的退出点,使得代码的维护异常艰难。

使用Optional

Person/Car/Insusrance的数据模型

public class Person {
    private Optional<Car> car;
    private int age;

    public Optional<Car> getCar() {
        return car;
    }

    public int getAge() {
        return age;
    }
}

public class Car {
    private Optional<Insurance> insurance;

    public Optional<Insurance> getInsurance() {
        return insurance;
    }
}

public class Insurance {
    private String name;

    public Insurance() {
    }
    public Insurance(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
public String getCarInsuranceName(Optional<Person> person) {
  // 2. 使用map从Optional对象中提取和转换值
  // 3. 使用flatMap链接Optional对象
  return person.flatMap(Person::getCar)
    .flatMap(Car::getInsurance)
    .map(Insurance::getName)
    .orElse("Unknown");
}

创建Optional的三种方法

声明一个空的Optional

// 1. 创建Optional对象
// 1.1 声明一个空的Optional
@Test(expected = NoSuchElementException.class)
public void testEmptyOptional() {
  Optional<Car> optCar = Optional.empty();
  System.out.println(optCar.get());
}

依据一个非空值创建Optional

// 1.2 依据一个非空值创建Optional
@Test(expected = NullPointerException.class)
public void testNotNullOptional() {
  Car car = null;
  Optional<Car> optCar = Optional.of(car);
}

可接受null的Optional

// 1.3 可接受null的Optional
@Test
public void testNullableOptional() {
  Car car = null;
  Optional<Car> optCar = Optional.ofNullable(car);
}

场景二:两个Optional对象的组合

假设需要这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:

public Insurance findCheapstInsurance(Person person, Car car) {
	// 不同的保险公司提供的查询服务,对比所有数据
  return cheapestCompany;
}

null-安全版本

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
  if (person.isPresent() && car.isPresent()) {
    return Optional.of(findCheapstInsurance(person.get(), car.get()));
  } else {
    return Optional.empty();
  }
}

这个方法的优势是,从它的签名就能非常清楚地知道无论是person还是car,它的值都有可能为空,出现这种情况时,方法的返回值也不会饮食任何值。但该方法的具体实现和之前实现的null检查太相似了,方法接受一个Person和一个Car对象作为参数,而二者都有可能为null。

以不解包的方式组合两个Optional对象

public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
  return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}

场景三:对Optional对象进行过滤

找出年龄大于或者等于minAge参数的Person所对应的保险公司列表

public String getCarInsuranceName(Optional<Person> person, int minAge) {
  return person.filter(p -> p.getAge() >= minAge)
    .flatMap(Person::getCar)
    .flatMap(Car::getInsurance)
    .map(Insurance::getName)
    .orElse("Unknown");
}

Optional类的方法

  • empty 返回一个空的Optional实例
  • filter 如果值存在并且满足提供的谓词,就返回饮食该值的Optional对象,否则返回一个空的Optional对象
  • flatMap 如果值存在,就对该值执行提供的mapping函数调用,返回一个Optional类型的值,否则就返回一个空的Optional对象
  • get 如果值存在,将该值用Optional封装返回,否则抛出一个NoSuchElementException异常
  • ifPresent 如果值存在,就执行使用该值的方法调用,否则什么也不做
  • isPresent 如果值存在就返回true,否则返回false
  • map 如果值存在,就对该值执行提供的mapping函数调用
  • of 将指定值值用Optional封装之后返回,如果该值为null,则抛出一个NullPointException异常
  • ofNullable 将指定值用Optional封装之后返回,如果该值为null,则返回一个空的Optional对象
  • orElse 如果有值则将其返回,否则返回一个默认值
  • orElseGet 如果有值则将其返回,否则返回一个由指定的Supplier接口生成的值
  • orElseThrow 如果有值则将其返回,否则抛出一个由指定的Supplier接口生成的异常

使用Optional的实战示例

用Optional封装可能为null的值

现存Java API几乎都是通过返回一个null的方式来表示需要值的缺失,或者由于某些原因计算无法得到该值。大多数情况下,我们更希望这些方法能返回一个Optional对象。我们无法修改这些方法的签名,但是可以用Optional对这些方法的返回值进行封装。

@Test
public void test_Value_wrapped_with_Optional_that_might_be_null() {
  Map<String, Object> map = new HashMap<>();

  Object value = map.get("key");
  Optional<Object> optValue = Optional.ofNullable(map.get("key"));
  System.out.println("value: " + value);
  System.out.println("optValue: " + optValue);
}

异常与Optional的对比

由于某些原因,函数无法返回某个值,这时除了返回null,Java API比较常见的替代做法是抛出一个异常。比较典型的例子是使用表态方法Integer.parseInt(String),将String转换为int。这个方法如果String无法解析到对应的整型,就会抛出一个NumberFormatException。我们也可以用空的Optional对象,对遭无法转换的String时返回的非法值进行建模,这时我们期望parseInt返回的值是一个optional。我们无法修改最初的Java方法,但是我们可以对它进行封装,我们可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的Optional对象。

public class OptionalUtility {
    public static Optional<Integer> stringToInt(String s) {
        try {
            return Optional.of(Integer.parseInt(s));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}