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类是怎样处理上述情形的。
Java 8 中引入了一个新的类java.util.Optional
如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,当某人没有车时把null引用赋值给它,而是应该像下图那样直接将其声明为Optional
使用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.empty(),创建一个空的Optional对象
Optional<Car> optCar = Optional.empty();
依据一个非空值创建Optional (不推荐)
如果car是一个null,这段代码会立即抛出一个NullPointerException,而不是等到你 试图访问car的属性值时才返回一个错误。
// 静态工厂方法Optional.of(T t),依据一个非空值创建一个Optional对象
Optional<Car> optCar = Optional.of(car);
// 用静态工厂方法Optional.ofNullable(T t),你可以创建一个允许null值的Optional对象
Optional<Car> optCar = Optional.ofNullable(car);
如果值存在,就对该值执行提供的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方法相差无几:
试试重构第一章中的代码:
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
对于这种嵌套式的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后的优点:
get() 是这些方法中最简单但又最不安全的方法。不推荐使用
如果变量存在,它直接返回封装的变量值,否则就抛出一个NoSuchElementException异常,不推荐使用。
p.s. orElse中调用的方法一直都会被执行,orElseGet方法只有在Optional对象不含值时才会被调用,所以使用orElse方法时需要谨慎, 以免误执行某些不被预期的操作
如果Optional对象包含值,该方法就返回true,否则为false
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"));
方法 | 描述 |
---|---|
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 接口生成的异常 |
现在,我们假设你有这样一个方法,它接受一个Person和一个Car对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司:
public Insurance findCheapestInsurance(Person person, Car car) {
// 不同的保险公司提供的查询服务
// 对比所有数据
return cheapestCompany;
}
我们还假设你想要该方法的一个null-安全的版本,它接受两个Optional对象作为参数, 返回值是一个Optional
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)));
}
相信你已经了解,有效地使用Optional类意味着你需要对如何处理潜在缺失值进行全面的反思。这种反思不仅仅限于你曾经写过的代码,更重要的可能是,你如何与原生Java API实现共存共赢。
实际上,我们相信如果Optional类能够在这些API创建之初就存在的话,很多API的设计编写可能会大有不同。为了保持后向兼容性,我们很难对老的Java API进行改动,让它们也使用 Optional,但这并不表示我们什么也做不了。你可以在自己的代码中添加一些工具方法,修复或者绕过这些问题,让你的代码能享受Optional带来的威力。
用Map做例子,假设你有一个Map
Object value = map.get("key");
可以采用我们前文介绍的Optional.ofNullable方法这段代码进行优化:
Optional<Object> value = Optional.ofNullable(map.get("key"));
每次你希望安全地对潜在为null的对象进行转换,将其替换为Optional对象时,都可以考 虑使用这种方法。
由于某种原因,函数无法返回某个值,这时除了返回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的逻辑了。
假设你需要向你的程序传递一些属性。为了举例以及测试你开发的代码,你创建了一些示例属性,如下所示:
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类设计时就没特别考虑将其作为类的字段使用,所以它也并未实现 Serializable接口。由于这个原因,如果你的应用使用了某些要求序列化的库或者框架,在域模型中使用Optional,有可能引发应用程序故障。
然而,我们相信,通过前面的介绍,你 已经看到用Optional声明域模型中的某些类型是个不错的主意,尤其是你需要遍历有可能全 部或部分为空,或者可能不存在的对象时。如果你一定要实现序列化的域模型,作为替代方案, 我们建议你像下面这个例子那样,提供一个能访问声明为Optional、变量值可能缺失的接口,代码清单如下:
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
Optional提供了的一些基础类型——OptionalInt、OptionalLong以及OptionalDouble. 但不推荐大家使用基础类型的Optional,因为基础类型的Optional不支持map、flatMap以及filter方法,而这些却是Optional类常用的方法。可以使用Optional
orElse中调用的方法一直都会被执行,orElseGet方法只有在Optional对象不含值时才会被调用,所以使用orElse方法时需要谨慎, 以免误执行某些不被预期的操作。此种情况下,可使用orElseGet方法代替它。