《Java8实战》第11章 用 Optional 取代 null

11.1 如何为缺失的值建模

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

上面的这种代码就很容易出现NullPointerException的异常。

11.1.1 采用防御式检查减少 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"; 
} 


或者
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的检查。

11.1.2 null 带来的种种问题
  • 它是错误之源。

NullPointerException 是目前 Java 程序开发中最典型的异常。

  • 它会使你的代码膨胀。

它让你的代码充斥着深度嵌套的 null 检查,代码的可读性糟糕透顶。

  • 它自身是毫无意义的。

null 自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。

  • 它破坏了 Java 的哲学。

Java 一直试图避免让程序员意识到指针的存在,唯一的例外是:null 指针。

  • 它在 Java 的类型系统上开了个口子。

null 并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题

11.1.3 其他语言中 null 的替代品

比如 Groovy,通过引入安全导航操作符(safe navigation operator,标记为?)可以安全访问可能为 null 的变量。为了理解它是如何工作的,让我们看看下面这段 Groovy代码,它的功能是获取某个用户替他的汽车保险的保险公司的名称:
def carInsuranceName = person?.car?.insurance?.name
这段代码的表述相当清晰。person 对象可能没有 car 对象,你试图通过赋一个 null 给Person 对象的 car 引用,对这种可能性建模。类似地,car 也可能没有 insurance。Groovy的安全导航操作符能够避免在访问这些可能为 null 引用的变量时抛出 NullPointerException,在调用链中的变量遭遇 null 时将 null 引用沿着调用链传递下去,返回一个 null。
关于 Java 7 的讨论中曾经建议过一个类似的功能,不过后来又被舍弃了。

11.2 Optional 类入门

Java 8 中引入了一个新的类 java.util.Optional。这是一个封装 Optional 值的类。
使用 Optional 重新定义 Person/Car/Insurance 的数据模型

public class Person { 
 private Optional<Car> car; // 人可能有汽车,也可能没有汽车,因此将这个字段声明为 Optional
 public Optional<Car> getCar() { return car; } 
} 
public class Car { 
 private Optional<Insurance> insurance; // 汽车可能进行了保险,也可能没有保险,所以将这个字段声明为 Optional 
 public Optional<Insurance> getInsurance() { return insurance; } 
} 
public class Insurance { 
 private String name; // 保险公司必须有名字
 public String getName() { return name; } 
} 

在你的代码中始终如一地使用 Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是算法上的缺陷,抑或是数据中的问题。另外,我们还想特别强调,引入 Optional 类的意图并非要消除每一个 null 引用。与此相反,它的目标是帮助你更好地设计出普适的 API,让程序员看到方法签名,就能了解它是否接受一个 Optional 的值。这种强制会让你更积极地将变量从 Optional 中解包出来,直面缺失的变量值。

11.3 应用 Optional 的几种模式

11.3.1 创建 Optional 对象
  1. 声明一个空的 Optional

通过静态工厂方法 Optional.empty 创建一个空的 Optional 对象:
Optional<Car> optCar = Optional.empty();

  1. 依据一个非空值创建 Optional

还可以使用静态工厂方法 Optional.of 依据一个非空值创建一个 Optional 对象:
Optional<Car> optCar = Optional.of(car);
如果 car 是一个 null,这段代码就会立即抛出一个 NullPointerException,而不是等到你试图访问 car 的属性值时才返回一个错误。

  1. 可接受 null 的 Optional

Optional<Car> optCar = Optional.ofNullable(car);
如果 car 是 null,那么得到的 Optional 对象就是个空对象。

11.3.2 使用 map 从 Optional 对象中提取和转换值

你可能想要从 insurance 公司对象中提取公司的名称。提取名称之前,你需要检查 insurance 对象是否为 null
Optional 也提供了一个 map 方法。它的工作方式如下
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
image.png

11.3.3 使用 flatMap 链接 Optional 对象

利用 map 重写之前的代码

Optional<Person> optPerson = Optional.of(person); 
Optional<String> name = optPerson.map(Person::getCar) 
 .map(Car::getInsurance) 
 .map(Insurance::getName); 

但这段代码无法通过编译
optPerson 是 Optional类型的变量,调用 map 方法应该没有问题。但 getCar 返回的是一个 Optional类型的对象(如代码清单 11-4 所示),这意味着 map 操作的结果是一个 Optional<Optional>类型的对象。
image.png

  1. 使用 Optional 获取 car 的保险公司名称
public String getCarInsuranceName(Optional<Person> person) { 
 return person.flatMap(Person::getCar) 
 .flatMap(Car::getInsurance) 
 .map(Insurance::getName) 
 .orElse("Unknown"); // 如果Optional的结果值为空,设置默认值
} 
  1. 使用 Optional 解引用串接的 Person/Car/Insurance 对象

image.png

11.3.4 操纵由 Optional 对象构成的 Stream

Java 9 引入了 Optional 的 stream()方法,使用该方法可以把一个含值的 Optional 对象转换成由该值构成的 Stream 对象,或者把一个空的 Optional 对象转换成等价的空 Stream。
找出 person 列表所使用的保险公司名称(不含重复项)
image.png

11.3.5 默认行为及解引用 Optional 对象

orElse方法默认值,当遭遇空的 Optional 变量时,默认值会作为该方法的调用返回值
Optional提供的变量

  • get() ,这个方法不太安全还是会抛出空指针异常的,
  • orElse(T other) 它允许你在Optional 对象不包含值时提供一个默认值。
  • orElseGet(Supplier<? extends="" t=""?> other)是 orElse 方法的延迟调用版,因为 Supplier 方法只有在 Optional 对象不含值时才执行调用
  • or(Supplier<? extends=""?><? extends="" t=""?>> supplier)与前面介绍的orElseGet 方法很像,不过它不会解包 Optional 对象中的值,即便该值是存在的。
  • orElseThrow(Supplier<? extends="" x=""?> exceptionSupplier)和 get 方法非常类似,它们遭遇 Optional 对象为空时都会抛出一个异常,但是使用 orElseThrow你可以定制希望抛出的异常类型
  • ifPresent(Consumer<? super="" t=""?>consumer)变量值存在时,执行一个以参数形式传入的方法,否则就不进行任何操作。

11.3.6 两个 Optional 对象的组合

方法,它接受一个 Person 和一个 Car 对象,设计一个普通版本和安全的版本

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

// 安全版本
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) { 
 if (person.isPresent() && car.isPresent()) { 
   return Optional.of(findCheapestInsurance(person.get(), car.get())); 
 } else { 
   return Optional.empty(); 
 } 
} 

但是这个安全版本的方法和之前的判null条件太像了

11.3.7 使用 filter 剔除特定的值

你经常需要调用某个对象的方法,查看它的某些属性。比如,你可能需要检查保险公司的名称是否为“Cambridge-Insurance”。

常规方式
Insurance insurance = ...; 
if(insurance != null && "CambridgeInsurance".equals(insurance.getName())){ 
 System.out.println("ok"); 
} 

使用Optional 对象的 filter 方法
Optional<Insurance> optInsurance = ...; 
optInsurance.filter(insurance -> "CambridgeInsurance".equals(insurance.getName())) 
 .ifPresent(x -> System.out.println("ok")); 

filter 方法接受一个谓词作为参数。如果 Optional 对象的值存在,并且它符合谓词的条件,filter 方法就返回其值;否则它就返回一个空的 Optional 对象。

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

image.png

11.4 使用 Optional 的实战示例

有效地使用 Optional 类意味着你需要对如何处理潜在缺失值进行全面的反思。

11.4.1 用 Optional 封装可能为 null 的值

大多数情况下,你可能希望这些方法能返回一个 Optional 对象。你无法修改这些方法的签名,但是你很容易用 Optional 对这些方法的返回值进行封装。
Object value = map.get("key");
如果没有该key,就是返回null。可以使用 Optional 封装 map 的返回值,
Optional<Object> value = **Optional.ofNullable**(map.get("key"));

11.4.2 异常与 Optional 的对比

函数无法返回某个值,这时除了返回 null,Java API 比较常见的替代做法是抛出一个异常。
典型的例子是使用静态方法 Integer.parseInt(String),将String 转换为 int。在这个例子中,如果 String 无法解析到对应的整型,该方法就抛出一个NumberFormatException。
与之前不同的是,这次你需要使用 try/catch 语句,而不是使用 if 条件判断来控制一个变量的值是否非空。
也可以用空的 Optional 对象,对遭遇无法转换的 String 时返回的非法值进行建模,这时你期望 parseInt 的返回值是一个 Optional。
我们修改不了之前的方法,但是我们可以封装一个新的工具方法

public static Optional<Integer> stringToInt(String s) { 
 try { 
	// 如果 String 能转换为对应的 Integer,将其封装在 Optional 对象中返回
   return Optional.of(Integer.parseInt(s)); 
 } catch (NumberFormatException e) { 
	//否则返回一个空的 Optional对象
   return Optional.empty(); 
 } 
}

我们的建议是,你可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。通过这种方式,你以后就能直接调用 OptionalUtility.stringToInt方法,将 String 转换为一个 Optional对象,而不再需要记得你在其中封装了笨拙的 try/catch 的逻辑了

11.4.3 基础类型的 Optional 对象,以及为什么应该避免使用它们

与 Stream 对象一样,Optional 也提供了类似的基础类型—— OptionalInt、OptionalLong 以及 OptionalDouble
有些地方可以不返回 Optional,而是直接返回一个 OptionalInt 类型的对象

11.4.4 把所有内容整合起来
Properties props = new Properties(); 
props.setProperty("a", "5"); 
props.setProperty("b", "true"); 
props.setProperty("c", "-3"); 

假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。由于一段时间必须是正数,你想要该方法符合下面的签名:
public int readDuration(Properties props, String name)
如果给定属性对应的值是一个代表正整数的字符串,就返回该整数值,任何其他的情况都返回 0。采用 JUnit 的断言

assertEquals(5, readDuration(param, "a")); 
assertEquals(0, readDuration(param, "b")); 
assertEquals(0, readDuration(param, "c")); 
assertEquals(0, readDuration(param, "d")); 

readDuration的实现

传统的命令方式
public int readDuration(Properties props, String name) { 
 String value = props.getProperty(name); 
 if (value != null) { // 确保名称对应的属性存在
   try { 
// 将 String 属性转换为数字类型
     int i = Integer.parseInt(value); 
     if (i > 0) { 
       return i; 
     } 
   } catch (NumberFormatException nfe) { } 
 } 
 return 0; 
}

如果需要访问的属性值不存在,Properties.getProperty(String)方法的返回值就是一个 null,使用 ofNullable 工厂方法可以方便地将该值转换为 Optional 对象。接着,你可以向它的 flatMap 方法传递代码清单 11-7 中实现的 OptionalUtility. stringToInt 方法的引用,将 Optional转换为 Optional。最后,你非常轻易地就可以过滤掉负数。这种方式下,如果任何一个操作返回一个空的 Optional 对象,该方法都会返回 rElse 方法设置的默认值 0;否则就返回封装在 Optional 对象中的正整数。

public int readDuration(Properties props, String name) { 
 return Optional.ofNullable(props.getProperty(name)) 
 .flatMap(OptionalUtility::stringToInt) 
 .filter(i -> i > 0) 
 .orElse(0); 
}

11.5 小结

  • null 引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
  • Java 8 中引入了一个新的类 java.util.Optional,对存在或缺失的变量值进行建模。
  • 你可以使用静态工厂方法 Optional.empty、Optional.of 以及 Optional.ofNullable创建 Optional 对象。
  • Optional 类支持多种方法,比如 map、flatMap、filter,它们在概念上与 Stream类中对应的方法十分相似。
  • 使用 Optional 会迫使你更积极地解引用 Optional 对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。
  • 使用 Optional 能帮助你设计更好的 API,用户只需要阅读方法签名,就能了解该方法是否接受一个 Optional 类型的值
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值