文章目录
一、Java 8 为什么引入Optional类?
null在Java中是一个合法值,目的是为了表示变量值的缺失。但对null的引用会引起NullPointerException(空指针异常)。在Java8以前,Java程序员操作对象时,为了避免错误引用null造成的空指针异常,往往需要一系列繁杂冗余的判空操作,增加了许多重复代码,降低了代码可读性,于是Java 8 引入Optional,优雅简洁的对null值进行处理。
为了便于理解和说明,本文将用以下的模型进行讲解:
假设有以下嵌套对象:人持有车,车持有保险,保险有名字属性。
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; }
}
假设要获取某一Person对象的车的保险的名字:
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
上面代码看似正常,但实际情况是getCar()和getInsurance()方法都可能返回null值,导致后续引用引起空指针异常。
Java 8 以前是怎么处理以保证null安全的呢:
1) 过多的 if 嵌套:
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";
}
或者:
2)过多的 return语句:
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();
}
接下来让我们进入下一章,了解Optional类,看看Optional类是怎样处理上述情形的。
二、什么是Optional类?
Java 8 中引入了一个新的类java.util.Optional<T>。可以理解为这是一个封装普通类对象(包括空值null)的容器类,Optional 对象最多只包含一个值。举例说明:
如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,当某人没有车时把null引用赋值给它,而是应该像下图那样直接将其声明为Optional<Car>类型。变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空” 的Optional对象,由方法Optional.empty()返回。
使用Optional重新定义Person/Car/Insurance的数据模型:
public class Person {
// 人可能有车,也可能没有车,因此将这个字段声明为Optional
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car {
// 车可能进行了保险,也可能没有保险,所以将这个字段声明为Optional
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
// 保险公司必须有名字, 因此没有使用Optional
private String name;
public String getName() { return name; }
}
优点:
-
Optional丰富了你模型的语义,这种方式非常清晰地表达了你的模型中一个person 可能拥有也可能没有car的情形,同样,car可能有保险,也可能没有保险;insurance必须含有名字。
-
在你的代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问 题,还是你算法上的缺陷,抑或是你数据中的问题。一旦获取insurance公司名称时发生NullPointerException,你就能非常确定地知道出错的原因,不再需要为其添加null的检查,因为null的检查只会掩盖问题,并未真正地修复问题。 insurance公司必须有个名字,所以,如果你遇到一个公司没有名称,你需要调查你的数据出了什么问题,而不应该再添加一段代码,将这个问题隐藏。
-
帮助你更好地设计出普适的API, 让程序员看到方法签名,就能了解它是否接受一个null的值
三、如何创建Optional对象
- 声明一个空的Optional
// 通过静态工厂方法Optional.empty(),创建一个空的Optional对象
Optional<Car> optCar = Optional.empty();
-
依据一个非空值创建Optional (不推荐)
如果car是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你 试图访问car的属性值时才返回一个错误。
// 静态工厂方法Optional.of(T t),依据一个非空值创建一个Optional对象
Optional<Car> optCar = Optional.of(car);
- 可接受null的Optional (推荐)
// 用静态工厂方法Optional.ofNullable(T t),你可以创建一个允许null值的Optional对象
Optional<Car> optCar = Optional.ofNullable(car);
四、如何从Optional对象中提取和转换值?
1. map方法
如果值存在,就对该值执行提供的mapping 函数调用, 如果值不存在,则返回一个空的Optional对象。
引入Optional 以前:
String name = null;
if(insurance != null){
name = insurance.getName();
}
引入Optional 后:
Optional<String> name = Optional.ofNullable(insurance)
.map(Insurance::getName);
Optional的map方法和 Java 8 中Stream的map方法相差无几:
2. flatMap方法
试试重构第一章中的代码:
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
由于我们刚刚学习了如何使用map,你的第一反应可能是我们可以利用map重写之前的代码:
Optional<Person> optPerson = Optional.of(person);
Optional<String> name =
optPerson.map(Person::getCar)
.map(Car::getInsurance)
.map(Insurance::getName);
不幸的是,这段代码无法通过编译。为什么呢? optPerson是Optional<Person>类型的 变量, 调用map方法应该没有问题。但getCar返回的是一个Optional<Car>**类型的对象,这意味着map操作的结果是一个Optional<Optional<Car>>类型的对象。因此,它对getInsurance的调用是非法的。上面代码map(Car :: getInsurance) 的返回结果如下图。
对于这种嵌套式的Optiona结构,我们应该使用flatMap方法,将两层的Optional合并成一个。
下面应用map和flatMap对上述示例进行重写:
使用Optional获取car的保险公司名称:
public String getCarInsuranceName(Optional<Person> person) {
return person.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown"); // 如果Optional的结果 值为空设置默认值
}
上述步骤可图解为:
对比第一章中Java 8 之前的代码和上面的代码,不难发现引入Optional后的优点:
- 不再需要使用那么多的条件分支,也不会增加代码的复杂性
五、如何获取Optional对象和设置默认值?
1. get()
-
get() 是这些方法中最简单但又最不安全的方法。不推荐使用
如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常,不推荐使用。
2. orElse(T other)
- orElse(T other) 它允许你在 Optional对象不包含值时提供一个默认值。
3. orElseGet(Supplier<? extends T> other)
- orElseGet(Supplier<? extends T> other)是orElse方法的延迟调用版,Supplier 方法只有在Optional对象不含值时才执行调用。如果创建默认值是件耗时费力的工作, 你应该考虑采用这种方式(借此提升程序的性能),或者你需要非常确定某个方法仅在 Optional为空时才进行调用,也可以考虑该方式(这种情况有严格的限制条件)。
4. orElseThrow(Supplier<? extends X> exceptionSupplier)
- orElseThrow(Supplier<? extends X> exceptionSupplier) 和get方法非常类似, 它们遭遇Optional对象为空时都会抛出一个异常,但是使用orElseThrow你可以定制希望抛出的异常类型。
5. ifPresent(Consumer<? super T> consumer)
- ifPresent(Consumer<? super T> consumer) 让你能在变量值存在时执行一个作为参数传入的 方法,否则就不进行任何操作。
p.s. orElse中调用的方法一直都会被执行,orElseGet方法只有在Optional对象不含值时才会被调用,所以使用orElse方法时需要谨慎, 以免误执行某些不被预期的操作
六、其他Optional对象的方法
1. isPresent方法
如果Optional对象包含值,该方法就返回true,否则为false
2. filter方法
filter方法接受一个谓词作为参数。如果Optional对象的值存在,并且它符合谓词的条件, filter方法就返回其值;否则它就返回一个空的Optional对象。
比如,你可能需要检查保险公司的名称是否为“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"));
七、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 接口生成的异常 |
八 、使用示例
1. 两个Optional对象的组合
现在,我们假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:
public Insurance findCheapestInsurance(Person person, Car car) {
// 不同的保险公司提供的查询服务
// 对比所有数据
return cheapestCompany;
}
我们还假设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数, 返回值是一个Optional<Insurance>对象,如果传入的任何一个参数值为空,它的返回值亦为空。Optional类还提供了一个isPresent方法,如果Optional对象包含值,该方法就返回true, 所以你的第一想法可能是通过下面这种方式实现该方法:
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();
}
}
你可以像使用三元操作符那样,无需任何条件判断的结构,以一行语句实现该方法,代码如下。
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c)));
}
2. Optional工具类的封装
相信你已经了解,有效地使用Optional类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共存共赢。
实际上,我们相信如果Optional类能够在这些API创建之初就存在的话,很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用 Optional,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复或者绕过这些问题,让你的代码能享受Optional带来的威力。
1)对Java API中返回null值的方法进行封装
用Map做例子,假设你有一个Map<String, Object>方法,访问由key索引的值时,如果map 中没有与key关联的值,该次调用就会返回一个null:
Object value = map.get("key");
可以采用我们前文介绍的Optional.ofNullable方法这段代码进行优化:
Optional<Object> value = Optional.ofNullable(map.get("key"));
每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考 虑使用这种方法。
2)对异常的封装
由于某种原因,函数无法返回某个值,这时除了返回null,Java API比较常见的替代做法是抛出一个异常。
比如:方法Integer.parseInt(String),将 String转换为int。在这个例子中,如果String无法解析到对应的整型,该方法就抛出一个 NumberFormatException。
我们无法修改最初的Java方法,但是这无碍我们进 行需要的改进,你可以实现一个工具方法,将这部分逻辑封装于其中,最终返回一个我们希望的 Optional对象,代码如下所示。
public static Optional<Integer> stringToInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
可以将多个类似的方法封装到一个工具类中,让我们称之为OptionalUtility。通过这种方式,你以后就能直接调用OptionalUtility.stringToInt方法,将String转换为一个Optional对象,而不再需要记得你在其中封装了笨拙的 try/catch的逻辑了。
3)工具类的使用示例
假设你需要向你的程序传递一些属性。为了举例以及测试你开发的代码,你创建了一些示例属性,如下所示:
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"));
让我们先以传统的方式实现满足这些需求的方法, 代码清单如下所示:
public int readDuration(Properties props, String name) {
String value = props.getProperty(name);
if (value != null) {
try {
int i = Integer.parseInt(value); //将String属性转 换为数字类型
if (i > 0) { //检查返回的数字是否为正数
return i;
}
} catch (NumberFormatException nfe) { }
}
return 0;
}
上面的实现既复杂又不具备可读性,让我们用之前封装的OptionalUtility来重写上面代码:
public int readDuration(Properties props, String name) {
return Optional.ofNullable(props.getProperty(name))
.flatMap(OptionalUtility::stringToInt)
.filter(i -> i > 0)
.orElse(0);
}
九、使用Optional需要注意的点
1. Optional的序列化问题
由于Optional类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现 Serializable接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在域模型中使用Optional,有可能引发应用程序故障。
然而,我们相信,通过前面的介绍,你 已经看到用Optional声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全 部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案, 我们建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,代码清单如下:
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
2. 避免使用基础类型的Optional对象
Optional提供了的一些基础类型——OptionalInt、OptionalLong以及OptionalDouble. 但不推荐大家使用基础类型的Optional,因为基础类型的Optional不支持map、flatMap以及filter方法,而这些却是Optional类常用的方法。可以使用Optional<Int>, Optional<Long>, Optional<Double>等替代。
3. orElse方法的使用
orElse中调用的方法一直都会被执行,orElseGet方法只有在Optional对象不含值时才会被调用,所以使用orElse方法时需要谨慎, 以免误执行某些不被预期的操作。此种情况下,可使用orElseGet方法代替它。