null 之殇
写 Java 程序,NullPointerException
应该说是非常常见的。Java 语言一直都是希望屏蔽底层指针操作,唯一的例外就是 null
指针,也就是空指针。空指针最大的问题是它违背了类型系统的初衷,相当于在 Java 类型系统上开了一个口子。
null
不属于任何类型。这意味着它可以被赋值给任何引用类型的变量。当这个变量被传递到系统中的另一个部分后,就无法获知这个 null
变量最初的赋值到底是什么类型。
2009年,在伦敦 QCon 会议上,快速排序的发明者 Tony Hoare 在演讲时对于发明了空指针进行了道歉:
I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.
在语言中引入空指针仅仅是因为它非常容易在编译器上实现,而不是有什么理论上的必要性。null
在之后困扰着整个软件行业,造成了无数的崩溃。同样,Java 语言不加思索地把 null
吸纳进了 Java 语言,它同样给 Java 程序员带来了不少的麻烦。
不少新兴语言都在语言层面上避免了 null
,比如:Rust、Groovy、Scala等等。Java 8 也希望能有一定的补救措施,于是引入了 Optional
类型。
传统上应对 null 的防御式检查
接下来,给出一个示例代码,场景是一个人(Person
)可能有汽车(Car
),也可能没有汽车;汽车可能有保险(Insurance
),也可能没有保险;但是,只要有保险,就一定有保险名字。
public class Person {
private Car car;
public Car getCar() {
return car;
}
}
public class Car {
private Insurance insurance;
public Insurance getInsurace() {
return insurance;
}
}
public class Insurance {
private String 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.getInsurace();
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.getInsurace();
if (insurance == null) {
return "Unknown";
}
return insurance.getName();
}
现在有四个截然不同的退出点,使得代码的维护非常困难,而且 Unknown
写多了,可能还会出现拼写错误。你可以抬杠说,IDE 现在会检查单词拼写,或者把它抽象到一个常量来避免这种问题。
好的,你说得对。进一步而言,这种检查还是很容易出错。如果你忘记检查哪个可能为 null
的属性会怎么样?
用 null
来表示变量值的缺失是大错特错的,我们需要更加优雅地方式来处理这个问题。
Optional 类
JVM 平台上的衍生语言 Scala 有一个数据结构 Option[T]
,它可以包含类型为 T
的变量也可以不包含该变量。Java 8从“optional值”(不一定是 Scala,别误会)中吸取了灵感,引入了 java.util.Optional<T>
的新类。
有了大致的 Optional
概念后,一个人(Person
)可能有车,也可能没车应该使用 Optional<Car>
,而不是没有车的时候使用 null
进行赋值。当变量存在时,Optional
类只是对类的简单封装,变量不存在时,缺失的值会被建模成一个“空”的 Optional 对象,由方法 Optional.empty()
返回。
Optional.empty()
方法是一个静态工厂方法,它返回 Optional
类的特定单一实例。null
和 Optional.empty()
差别还是非常大的,如果你视图解引用一个 null
,一定会触发 NullPointerException
,不过使用 Optional.empty()
就完全没事。
创建 Optional 对象
(1). 声明一个空的 Optional
Optional<Car> optCar = Optional.empty();
(2). 依据一个非空值创建 Optional
Optional<Car> optCar = Optional.of(car);
如果 car
是一个 null
,这段代码会立即抛出 NullPointerException
,而不是等到你试图访问 car
的属性值时才返回一个错误。
(3). 可接受 null 的 Optional
使用静态工厂方法 Optional.ofNullable
,可以创建一个允许 null
值的 Optional 对象。
Optional<Car> optCar = Optional.ofNullable(car);
如果 car
是 null
,那么得到的 Optional
对象就是个空对象。
将之前的 Person
、Car
、Insurance
用 Optional 重构一下:
public class Person {
private Optional<Car> optCar;
Person(Car car) {
this.optCar = Optional.ofNullable(car);
}
public Optional<Car> getOptCar() {
return optCar;
}
}
public class Car {
private Optional<Insurance> optInsurance;
Car(Insurance insurance) {
this.optInsurance = Optional.ofNullable(insurance);
}
public Optional<Insurance> getOptInsurance() {
return optInsurance;
}
}
public class Insurance {
private String name;
Insurance(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
获取 Optional 对象的值
提取 insurance
对象之前,需要检查对象是否为 null
:
String name = null;
if (insurance != null) {
name = insurance.getName();
}
为了完成这种模式,Optional 提供了一个 map
方法。
Optional<Insurance> optInsurance = Optional.ofNullable(insurance);
Optional<String> name = optInsurance.map(Insurance::getName);
如果想用 Optional 重构之前的这段代码:
public String getCarInsuranceName(Person person) {
return person.getCar().getInsurance().getName();
}
介绍过 map
,一个很自然的反应是这样的:
Optional<Person> optPerson = Optional.of(person);
Optional<String> name = optPerson.map(Person::getOptCar)
.map(Car::getOptInsurance)
.map(Insurance::getName);
然而,不幸的是,上面这段代码没法通过编译。
optPerson
是 Optional<Person>
类型的变量,调用 map
没有什么问题。但是 getCar
返回的是一个 Optional<Car>
类型的对象。这意味着 map
操作的结果是一个 Optional<Optional<Car>>
对象。因此对它使用 getOptInsurance
的调用是非法的。
要解决这个问题,显然是把这个嵌套的 Optional<Optional<Car>>
展开成 Optional<Car>
,所以要用 flatMap
。flatmap
会用流的内容替换每个新生成的流。换句话说,由方法生成的各个流会被合并或者扁平化成单一的流。
public String getCarInsuranceName(Optional<Person> optPerson) {
return optPerson.flatMap(Person::getOptCar)
.flatMap(Car::getOptInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}
可以看到,处理潜在可能缺失的值时,使用 Optional
具有明显的优势。另外,编程语言首要的功能就是沟通,通过 Optional 类型提供的语义,很清楚它是否接受空值,或者可能返回空值。
使用 orElse
方法读取这个变量的值,遭遇到空 Optional 变量时,默认值会作为该方法的调用返回值。Optional 类提供了多种方法读取 Optional 实例中的变量值。
get()
: 最简单但最不安全的方法。如果变量存在,直接返回封装的变量值,否则抛出NoSuchElementException
异常。除非你确定 Optional 变量一定包含值,否则这个方法没什么太大的用处。(与null
相比,并没有明显的改进)orElse(T other)
: 前面提到的,允许在 Optional 对象不包含值的时候提供一个默认值。orElseGet(Supplier<? extends T> other)
:orElse
方法的延迟调用版,Supplier
方法只有在 Optional 对象不含值时才执行调用。(如果创建默认值是件耗时费力的工作,应该考虑这种方式)。orElseThrow(Supplier<? extends X> exceptionSupplier)
:与get
方法类似,Optional 对象为空时会抛出一个异常,只不过可以定制希望的抛出的异常类。ifPresent(Consumer<? super T>)
:在变量值存在时执行一个作为参数传入的方法,否则不进行任何操作。
序列化的问题
如果在 Intellij IDEA 上尝试使用上述代码,IDE 可能会把作为类变量的 Optional<XXX>
标黄,并提示它作为类型(字段)的时候可能会出现序列化的问题。
在这之前,我们使用 Optional 将缺失或者无定义的变量值用特殊的形式标记出来。然而,Optional 类的设计者的初衷并非如此。Java 语言架构师 Brian Goetz 曾经非常明确地陈述过,Optional 的设计初衷仅仅是要支持能返回 Optional 对象的语法。(这也是 IDE 给出的解释)
由于 Optional 没有考虑作为类的字段来使用,所以没有实现 Serializable 接口。如果程序中需要用到序列化的库或者框架,使用 Optional,可能引发程序出错。如果你一定要实现序列化的模型,作为替代方案,建议像下面这样,提供一个能访问声明为 Optional、变量值可能缺失的接口:
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
Optional 对象的组合
假设有这样一个方法,它接受一个 Person
和 Car
对象,并以此为条件对外部提供的服务进行查询,通过一些复杂的业务逻辑,试图找到满足该组合的最便宜的保险公司。
不安全的方法就略过啦。现在希望有一个接受两个 Optional 对象作为参数,返回值是一个 Optional<Insurance>
对象。
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> optPerson, Optional<Car> optCar) {
if (optPerson.isPresent() && optCar.isPresent()) {
return Optional.of(findCheapestInsurance(optPerson.get(), optCar.get()));
} else {
return Optional.empty();
}
}
这个方法有明显的优势,从参数就可以看出它既可以为空也可以有值,返回值同样有明显的语义。然而,它的具体实现和 null 检查太相似了。
利用 Java 8 带来的函数式编程的特性还有 Optional 的 flatMap
方法,可以写出一段非常非常酷酷的代码:
public Optional<Insurance> nullSafeFindCheapestInsurance(Optional<Person> optPerson, Optional<Car> optCar) {
return optPerson.flatMap(p->optCar.map(c->findCheapestInsurance(p, c)));
}
这段代码的执行过程是这样的,首先 optPerson
调用 flatMap
,如果 optPerson
对象存在,那么就会执行传入的 Lambda 表达式。optCar
如果存在,同样作为参数调用 findCheapestInsurance
此时。这个时候是可以保证 person
和 car
的存在,所以可以安全地调用,完成期望的操作。
filter 剔除特定的值
Optional 类和 Stream
接口有很多相似的地方,它也有个 filter
方法。比如,你可能需要检查保险公司的名称是否为“ChinaLifeInsurance”,为了用一种安全的方式进行这种操作,首先要确定引用指向的 Insurance
对象是否为 null
,之后再调用 getName
方法:
Insurance insurance = ...;
if (insurance != null && "ChinaLifeInsurance".equals(insurance.getName())) {
System.out.println("ok");
}
使用 Optional 对象的 filter
方法,可以重构如下:
Optional<Insurance> optInsurance = ...;
optInsurance.filter(insurance -> "ChinaLifeInsurance".equals(insurance.getName()))
.ifPresent(x -> System.out.println("ok"));
filter
方法接受一个谓词作为参数。如果 Optional 对象的值存在,并且它符合谓词的条件,filter
方法就返回其值;否则,它就返回一个空的 Optional
对象。
基于 filter
可以更为有效地以非过程式进行编程。例如,找出年龄大于等于 minAge
参数的 Person
所对应的保险公司列表。这种方式就和 SQL 非常地相似。
public String getCarInsuranceName(Optional<Person> person, int minAge) {
return person.filter(p -> p.getAge() >= minAge)
.flatMap(Person::getCar)
.flatMap(Car::getOptInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}
Optional 实战
用 Optional 封装可能为 null 的值
现存的 Java API 几乎都是通过返回一个 null
的方式来表示值的缺失,或者由于某些原因计算无法得到该值。比如,Map
中不含指定的键对应的值,它的 get
方法会返回一个 null
。但这些方法的参数早就确定了,你也很难去改,但是你很容易用 Optional
对返回值进行封装。假设你现在有一个 Map<String, Object>
方法,访问由 key
索引的值时,如果 map
中没有与 key
关联的值,它就会返回 null
。
Object value = map.get("key");
Optional<Object> value = Optional.ofNullable(map.get("key"));
每次你希望安全地对潜在为 null 的对象进行转换,将其替换为 Optional
对象时,都可以考虑使用这种方法。
异常与 Optional 的对比
由于某种原因,函数无法返回某个值,这时除了返回 null
,Java API 比较常见的替代做法是抛出一个异常。这种情况比较典型的例子是静态方法 Integer.parseInt(String)
,将 String
转换为 int
。如果 String
无法解析到对应的整型,该方法就抛出一个 NumberFormatException
。最后的效果是,发生 String
无法转换为 int
,代码发出一个遭遇非法参数的信号。这时,你需要用 try/catch
语句,而不是使用 if
条件判断来控制一个变量的值是否为空。
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
。就不再需要操心 try/catch了。
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 接口生成的异常 |
小结
null
一开始引入程序设计语言,目的当然是表示变量值的缺失,但它不是必要的设计,带来了不计其数的空指针异常问题和程序崩溃的问题。Java 8 引入了新的类 java.util.Optional<T>
,对存在或缺失的变量值进行建模。使用 Optional 可以有效地防止代码中出现不期而至的空指针异常。