开启掘金成长之旅!这是我参与「掘金日新计划 · 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,否则返回falsemap如果值存在,就对该值执行提供的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();
}
}
}