假设需要处理下面这样的嵌套对象,这是一个拥有汽车及汽车保险的客户。
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();
}
在实践中,一种比较常见的做法是返回一个null
引用,表示该值的缺失,即用户没有车。而接下来,对getInsurance
的调用会返回null
引用的insurance
,这会导致运行时出现一个NullPointerException
,终止程序的运行。但这还不是全部。如果返回的person
值为null
会怎样?如果getInsurance
的返回值也是null
,结果又会怎样?
怎样做才能避免这种不期而至的NullPointerException
呢?通常,可以在需要的地方添加null
的检查(过于激进的防御式检查甚至会在不太需要的地方添加检测代码),并且添加的方式往往各有不同。下面这个例子是试图在方法中避免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";
}
这个方法每次引用一个变量都会做一次null
检查,如果引用链上的任何一个遍历的解变量值为null
,它就返回一个值为“Unknown
”的字符串。唯一的例外是保险公司的名字,不需要对它进行检查,原因很简单,因为任何一家公司必定有个名字。将上面的代码标记为“深层质疑”,原因是它不断重复着一种模式:每次不确定一个变量是否为null
时,都需要添加一个进一步嵌套的if
块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性。面对这种窘境,可以尝试另一种方案。下面的代码清单中,试图通过一种不同的方式避免这种问题。
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();
}
第二种尝试中,试图避免深层递归的if
语句块,采用了一种不同的策略:每次遭遇null
变量,都返回一个字符串常量“Unknown
”。然而,这种方案远非理想,现在这个方法有了四个截然不同的退出点,使得代码的维护异常艰难。更糟的是,发生null
时返回的默认值,即字符串“Unknown
”在三个不同的地方重复出现——出现拼写错误的概率不小!当然,可以用把它们抽取到一个常量中的方式避免这种问题。进一步而言,这种流程是极易出错的;如果忘记检查了那个可能为null
的属性会怎样?使用null
来表示变量值的缺失是大错特错的。需要更优雅的方式来对缺失的变量值建模。
在Java程序开发中使用null会带来理论和实际操作上的种种问题。
它是错误之源。
NullPointerException
是目前Java
程序开发中最典型的异常。
它会使你的代码膨胀。
它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。
它自身是毫无意义的。
null
自身没有任何的语义,尤其是,它代表的是在静态类型语言中以一种错误的方式对缺失变量值的建模。
它破坏了Java的哲学。
Java
一直试图避免让程序员意识到指针的存在,唯一的例外是:null
指针。
它在Java的类型系统上开了个口子。
null
并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,原因是当这个变量被传递到系统中的另一个部分后,将无法获知这个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
程序员碰到NullPointerException
时的第一冲动就是添加一个if
语句,在调用方法使用该变量之前检查它的值是否为null
,快速地搞定问题。如果按照这种方式解决问题,丝毫不考虑算法或者数据模型在这种状况下是否应该返回一个null
,其实并没有真正解决这个问题,只是暂时地掩盖了问题,使得下次该问题的调查和修复更加困难。刚才的那种方式实际上是掩耳盗铃,只是在清扫地毯下的灰尘。而Groovy
的null
安全解引用操作符也只是一个更强大的扫把。你不会忘记做这样的检查,因为类型系统会强制进行这样的操作。另一些函数式语言,比如Haskell、Scala
,试图从另一个角度处理这个问题。Haskell
中包含了一个Maybe
类型,它本质上是对optional
值的封装。Maybe
型的变量可以是指定类型的值,也可以什么都不是。但是它并没有null
引用的概念。Scala
有类似的据结构,名字叫Option[T]
,它既可以包含类型为T
的变量,也可以不包含该变量。要使用这种类型,你必须显式地调用Option
类型的available
操作,检查该变量是否有值,而这其实也是一种变相的“null
检查”。Java 8
从“optional
值”的想法中吸取了灵感,引入了一个名为java.util.Optional
的新的类。后续会展示使用这种方式对可能缺失的值建模,而不是直接将null
赋值给变量所带来的好处。还会阐释从null
到Optional
的迁移,需要思考的是:如何在代码中使用optional
值。