Optional
本章内容
- 如何为缺失的值建模
- Optional 类
- 应用Optional的几种模式
- 使用Optional的实战实例
- 小结
如何为缺失的值建模
exp:
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方法的结果会怎样呢?在实践中,一种比较常见的做法是返回一个null引用,表示该值的缺失,即用户没有车。而接下来,对getInsurance的调用会返回null引用的insurance,这会导致运行时出现
一个NullPointerException,终止程序的运行。但这还不是全部。如果返回的person值为null会怎样?如果getInsurance的返回值也是null,结果又会怎样?
采用防御式检查减少 NullPointerException,exp:
public String getInstanceName1(Person person) {
if(person != null) {
Car car = person.getCar();
if(car != null) {
Insurance insurance = car.getInsurance();
if(insurance != null) {
return insurance.getName();
}
}
}
return DEFAULT_INSTANCE_NAME;
}
上面代码清单为“深层质疑”,原因是它不断重复着一种模式:每次你不确定一
个变量是否为null时,都需要添加一个进一步嵌套的if块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性。
解决这种嵌套过深可以使用卫语句来解决,exp:
public String getInstanceName2(Person person) {
if(person == null) {
return DEFAULT_INSTANCE_NAME;
}
Car car = person.getCar();
if(car == null) {
return DEFAULT_INSTANCE_NAME;
}
Insurance insurance = car.getInsurance();
if(insurance == null) {
return DEFAULT_INSTANCE_NAME;
}
return insurance.getName();
}
上面代码虽然解决了嵌套过深问题,然而,这种方案远非理想,现在这个方法有了四个截然不同的退出点,使得代码的维护异常艰难。而且这种流程是极易出错的;如果你忘记检查了那个可能为null的属性会怎样
null 带来的种种问题
- 它是错误之源。
NullPointerException是目前Java程序开发中最典型的异常。 - 它会使你的代码膨胀。
它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。 - 它自身是毫无意义的。
null自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。 - 它破坏了Java的哲学。
Java一直试图避免让程序员意识到指针的存在,唯一的例外是: null指针。 - 它在Java的类型系统上开了个口子。
null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的赋值到底是什么类型。
其他语言中null的替代品
- Groovy : 通过引入安全导航操作符(Safe Navigation Operator,标记为?)可以安全访问可能为null的变量.
exp :def carInsuranceName = person?.car?.insurance?.name
Groovy的安全导航操作符能够避免在访问这些可能为null引用的变量时抛出NullPointerException,在调用链中的变量遭遇null时将null引用沿着调用链传递下去,返回一个null。 - Haskell中包含了一个Maybe类型,它本质上是对optional值的封装。Maybe类型的变量可以是指定类型的值,也可以什么都不是。但是它并没有null引用的概念。Scala有类似的数据结构,名字叫Option[T],它既可以包含类型为T的变量,也可以不包含该变量; 要使用这种类型,你必须显式地调用Option类型的available操作,检查该变量是否有值,而这其实也是一种变相的“null检查”
java8 Optional
汲取Haskell和Scala的灵感, Java 8中引入了一个新的类java.util.Optional<T>
。这是一个封装Optional值的类。变量存在时, Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由方法Optional.empty()
返回。Optional.empty()方法是一个静态工厂方法,它返回Optional类的特定单一实例。
null引用和Optional.empty()有什么本质的区别吗?
- 不会触发NullPointerException。
- 使用Optional而不是null的一个非常重要而又实际的语义区别是:如声明变量时使用的是Optional<Car>类型,而不是Car类型,这句声明非常清楚地表明了这发生变量缺失是允许的;与此相反,使用Car这样的类型,可能将变量赋值为null,这意味着你需要独立面对这些,你只能依赖你对业务模型的理解,判断一个null是否属于该变量的有效范畴。
你的代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是你算法上的缺陷,抑或是你数据中的问题。另外,我们还想特别强调,引入Optional类的意图并非要消除每一个null引用。与此相反,它的目标是帮助你更好地设计出普适的API,让程序员看到方法签名,就能了解它是否接受一个Optional的值。这种强制会让你更积极地将变量从Optional中解包出来,直面缺失的变量值。
应用 Optional 的几种模式
创建Optional对象
- 声明一个空的Optional
Optional<Car> optCar = Optional.empty();
- 依据一个非空值创建Optional
Optional<Car> optCar = Optional.of(car);
如果car是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你试图访问car的属性值时才返回一个错误。
- 可接受null的Optional
Optional<Car> optCar = Optional.ofNullable(car);
- 使用 map 从 Optional 对象中提取和转换值
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
- 使用 flatMap 从 Optional 对象提取值
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
String name = optInsurance.flatMap(Insurance::getName);
那对上面的例子可以重新建模 exp:
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() {
return insurance;
}
public void setInsurance(Optional<Insurance> insurance) {
this.insurance = insurance;
}
}
////////////////
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() {
return car;
}
public void setCar(Optional<Car> car) {
this.car = car;
}
}
//////////////////////
public class Insurance {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
获取保险公司名称,如何使用Optional获取呢? exp:
public String getInstanceName1(Optional<Person> person) {
if(person.isPresent()) {
Optional<Car> car = person.get().getCar();
if(car.isPresent()) {
Optional<Insurance> insurance = car.get().getInsurance();
if(insurance.isPresent()) {
return insurance.get().getName();
}
}
}
return UNKNOWN_INSTANCE_NAME;
}
这种方式和null判断本质是一样的,没有解决问题。
**真正的解法 使用flatMap,map 来提取转换值 exp: **
public String getInstanceName2(Optional<Person> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse(UNKNOWN_INSTANCE_NAME);
}
我们决定采用orElse
方法读取这个变量的值,使用这种方式你还可以定义一个默认值,遭遇空的Optional变量时,默认值会作为该方法的调用返回值。Optional类提供了多种方法读取Optional实例中的变量值。
- get() 是这些方法中最简单但又最不安全的方法。如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常。所以,除非你非常确定Optional变量一定包含值,否则使用这个方法是个相当糟糕的主意。此外,这种方式即便相对于嵌套式的null检查,也并未体现出多大的改进。
- orElse(T other) 它允许你在Optional对象不包含值时提供一个默认值。
- orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作,你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。
- orElseThrow(Supplier<? extends X> exceptionSupplier)和get方法非常类似,它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型
- ifPresent(Consumer<? super T>) 让你能在变量值存在时执行一个作为参数传入的方法,否则就不进行任何操作。
两个 Optional 对象的组合
假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外
部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:
public Insurance findCheapestInsurance(Person person, Car car) {
// 不同的保险公司提供的查询服务
// 对比所有数据
return cheapestCompany;
}
设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数,
返回值是一个Optional<Insurance>
对象,如果传入的任何一个参数值为空,它的返回值亦为空 exp:
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检查太相似了,有没有更优雅的方案呢?
我们可以像使用三元操作符那样,无需任何条件判断的结构,以一行语句实现该方法,代码如下。exp:
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
这段代码中,你对第一个Optional对象调用flatMap方法,如果它是个空值,传递给它的Lambda表达式不会执行,这次调用会直接返回一个空的Optional对象。反之,如果person对象存在,这次调用就会将其作为函数Function的输入,并按照与flatMap方法的约定返回一个Optional<Insurance>对象。这个函数的函数体会对第二个Optional对象执行map操作,如果第二个对象不包含car,函数Function就返回一个空的Optional对象,整个nullSafeFindCheapestInsuranc方法的返回值也是一个空的Optional对象。最后,如果person和car对象都存在,作为参数传递给map方法的Lambda表达式能够使用这两个值安全地调用原始的findCheapestInsurance方法,完成期望的操作。
Optional类的方法
方法 | 描述 |
---|---|
empty | 返回一个空的 Optional 实例 |
filter | 如果值存在并且满足提供的谓词,就返回包含该值的 Optional 对象;否则返回一个空的Optional 对象 |
flatMap | 如果值存在,就对该值执行提供的 mapping 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象 |
get | 如果该值存在,将该值用 Optional 封装返回,否则抛出一个 NoSuchElementException 异常 |
ifPresent | 如果值存在,就执行使用该值的方法调用,否则什么也不做 |
isPresent | 如果值存在就返回 true,否则返回 false |
map | 如果值存在,就对该值执行提供的 mapping 函数调用 |
of | 将指定值用 Optional 封装之后返回,如果该值为 null,则抛出一个 NullPointerException异常 |
ofNullable | 将指定值用 Optional 封装之后返回,如果该值为 null,则返回一个空的 Optional 对象 |
orElse | 如果有值则将其返回,否则返回一个默认值 |
orElseGet | 如果有值则将其返回,否则返回一个由指定的 Supplier 接口生成的值 |
orElseThrow | 如果有值则将其返回,否则抛出一个由指定的 Supplier 接口生成的异常 |
使用 Optional 的实战示例
有效地使用Optional类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共存共赢。实际上,我们相信如果Optional类能够在这些API创建之初就存在的话,很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用Optional,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复或者绕过这些问题,让你的代码能享受Optional带来的威力。exp:
- 用 Optional 封装可能为 null 的值,比如从map获取值
Optional<Object> value = Optional.ofNullable(map.get("key"))
每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考虑使用这种方法。
- 异常与 Optional 的对比
由于某种原因,函数无法返回某个值,这时除了返回null, Java API比较常见的替代做法是抛出一个异常。exp:Integer.parseInt(String)
如果String无法解析到对应的整型,该方法就抛出一个NumberFormatException;我们可以空的Optional对象,对遭遇无法转换的String时返回的非法值进行建模。exp:
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
我们可以将多个类似的方法封装到一个工具类中,如OptionalUtility.stringToInt
.
注: 与 Stream 对象一样,Optional也提供了类似的基础类型——OptionalInt、 OptionalLong以及OptionalDouble。在Stream的场景,尤其是如果Stream对象包含了大量元素,出于性能的考量,使用基础类型是不错的选择,但对Optional对象而言,这个理由就不成立了,因为Optional对象最多只包含一个值。不推荐大家使用基础类型的Optional,因为基础类型的Optional不支持map、flatMap以及filter方法,而这些却是Optional类最有用的方法,此外,与Stream一样, Optional对象无法由基础类型的Optional组合构成
把所有内容整合起来
Properties props = new Properties();
props.setProperty("a", "5");
props.setProperty("b", "true");
props.setProperty("c", "-3");
现在,我们假设你的程序需要从这些属性中读取一个值,该值是以秒为单位计量的一段时间。由于一段时间必须是正数,你想要该方法符合下面的签名:
public int readDuration(Properties props, String name)
如果以命令式编程的方式从属性中读取duration值,exp:
public int readDuration(Properties props, String name) {
String value = props.getProperty(name);
if (value != null) {
try {
int i = Integer.parseInt(value);
if (i > 0) {
return i;
}
} catch (NumberFormatException nfe) { }
}
return 0;
}
可以看出最终的实现既复杂又不具备可读性,呈现为多个由if语句及try/catch
块儿构成的嵌套条件。
以Optional来实现呢exp:
public int readDuration(Properties props, String name) {
return Optional.ofNullable(props.getProperty(name))
.flatMap(OptionalUtility::stringToInt)
.orElse(0);
}
Optional 问题
在域模型中使用Optional,由于没有实现Serializable 接口,不能进行序列化
小结
- null引用在历史上被引入到程序设计语言中,目的是为了表示变量值的缺失。
- Java 8中引入了一个新的类java.util.Optional<T>,对存在或缺失的变量值进行建模。
- 你可以使用静态工厂方法Optional.empty、Optional.of以及Optional.ofNullable创建Optional对象。
- Optional类支持多种方法,比如map、flatMap、filter,它们在概念上与Stream类中对应的方法十分相似。
- 使用Optional会迫使你更积极地解引用Optional对象,以应对变量值缺失的问题,最终,你能更有效地防止代码中出现不期而至的空指针异常。
- 使用Optional能帮助你设计更好的API,用户只需要阅读方法签名,就能了解该方法是否接受一个Optional类型的值。