前言

在Java中进行null校验几乎人人都会,但是本文能归类出三种写作手法,可谓是艺术化加工。跟茴香豆有几种写法一样,虽然本文的内容不会让你有太大的实质性提升,但是开阔了眼界,启发了思维,还是不错的。读完本文,你会从重复搬砖的码农,摇身一变,成为充满艺术气息的工程师,跨界体验一下也是不错的。

正文

对任何一位Java程序员来说,无论是初出茅庐的新人,还是久经江湖的专家,NullPointerException都属于心中之痛,可是我们又无能为力,因为它是为了使用方便所付出的代价。

1、null的起源

null 引用的发明者 Tony Hoare 在 2009 年道歉,并称这种错误可导致十亿美元的损失:

我将其称之为自己的十亿美元错误。它的发明是在1965 年,那时我用一个面向对象语言(ALGOL W)设计了第一个全面的引用类型系统。我的目的是确保所有引用的使用都是绝对安全的,编译器会自动进行检查。但是我未能抵御住诱惑,加入了 null 引用,仅仅是因为实现起来非常容易。它导致了数不清的错误、漏洞和系统崩溃,可能在之后 40 年中造成了十亿美元的损失。

补充说明:
话说回来,没有null那这个世界将变得非常不方便,用Optional岂不是把人急死。

2、null带来的种种问题

在Java程序开发中使用null会带来种种问题。

(1)它是错误之源。
NullPointerException是目前Java程序开发中最典型的异常。

(2)它会使你的代码膨胀。
它让你的代码充斥着深度嵌套的null检查,代码的可读性糟糕透顶。

(3)它自身是毫无意义的。
null自身没有任何的语义,尤其是在静态类型语言中,它代表的是以一种错误的方式对缺失变量值的建模。

(4)它破坏了Java的哲学。
Java一直试图避免让程序员意识到指针的存在,唯一的例外是:null指针。

补充说明:
Java就是一个矛盾体,Java的初衷是屏蔽底层,但是很多Java开发者却以研究底层为乐,抱着JVM当亲爹。嗯,也对,马克思主义哲学就说过:矛盾是对立统一的。

(5)它在Java的类型系统上开了个口子。
null并不属于任何类型,这意味着它可以被赋值给任意引用类型的变量。这会导致问题,当这个变量被传递到系统中的另一个部分后,你将无法获知这个null变量最初的赋值到底是什么类型。

补充说明:
开口子是Java的强项,双亲委派之后,又冒出一个线程上下文类加载器

3、null检查

无论如何,我们必须要面对它。所以,我们到底能做些什么来防止 NullPointerException 异常呢?答案显然是对其添加 null 检查。

看下面的代码:

class Person
{
    private Car car;

    public Car getCar()
    {
        return car;
    }
}

class Car
{
    private Insurance insurance;

    public Insurance getInsurance()
    {
        return insurance;
    }
}

class Insurance
{
    private String name;

    public String getName()
    {
        return name;
    }
}

那么,下面这段代码存在怎样的问题呢?

public static String getCarInsuranceName(Person person)
{
    return person.getCar().getInsurance().getName();
}

这段代码看起来相当正常,但是现实生活中很多人没有车,有车的人未必就上了保险。所以以上代码往往会出现NullPointerException异常,终止程序的运行。如何进行null检查判断呢,有下面几种方式。

3.1、深层质疑

    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”的字符串。唯一的例外是保险公司的名字,你不需要对它进行检查,原因很简单,因为任何一家公司必定有个名字。请注意,由于你掌握业务领域的知识,避免了最后这个检查,但这并不会直接反映在你建模数据的Java类之中。

我们将上述代码称为“深层质疑”,原因是它不断重复着一种模式:每次你不确定一个变量是否为null时,都需要添加一个进一步嵌套的if块,也增加了代码缩进的层数。很明显,这种方式不具备扩展性,同时还牺牲了代码的可读性。

3.2、过多的退出语句

    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来表示变量值的缺失是大错特错的。你需要更优雅的方式来对缺失值的变量赋值。

3.3、使用Optional重新定义Person/Car/Insurance的数据模型

汲取Haskell和Scala的灵感,Java 8中引入了一个新的类java.util.Optional<T>。这是一个封装Optional值的类。举例来说,如果你知道一个人可能有也可能没有车,那么Person类内部的car变量就不应该声明为Car,遭遇某人没有车时把null引用赋值给它,而是应该像下图那样直接将其声明为Optional<Car>类型。

optional.png

变量存在时,Optional类只是对类简单封装。变量不存在时,缺失的值会被建模成一个“空”的Optional对象,由方法Optional.empty()返回。Optional.empty()方法是一个静态工厂方法,它返回Optional类的特定单一实例。你可能还有疑惑,null引用和Optional.empty()
有什么本质的区别吗?从语义上,你可以把它们当作一回事儿,但是实际中它们之间的差别非常大:如果你尝试引用一个null,一定会触发NullPointerException,不过使用Optional.empty()就完全没事儿,它是Optional类的一个有效对象,多种场景都能调用,非常有用。

使用Optional而不是null的一个非常重要而又实际的语义区别是,我们在声明变量时使用的是Optional<Car>类型,而不是Car类型,这句声明非常清楚地表明了这里发生变量缺失是允许的。与此相反,使用Car这样的类型,可能将变量赋值为null,这意味着你需要独立面对这些,你只能依赖自己对业务模型的理解,判断一个null是否属于该变量的有效范畴。

class Person
    {
        private Optional<Car> car;

        public Optional<Car> getCar()
        {
            return car;
        }
    }

    class Car
    {
        private Optional<Insurance> insurance;

        public Optional<Insurance> getInsurance()
        {
            return insurance;
        }
    }

    class Insurance
    {
        private String name;

        public String getName()
        {
            return name;
        }
    }

代码中Person引用的是Optional<Car>,而Car引用的是Optional<Insurance>,这种方式非常清晰地表达了你的模型中一个Person可能拥有也可能没有Car的情形,同样,Car可能购买了保险,也可能没有保险。

与此同时,我们看到Insurance公司的名称被声明成String类型,而不是Optional<String>,这非常清楚地表明Insurance公司必须提供公司名称。使用这种方式,一旦引用Insurance公司名称时发生NullPointerException,你就能非常确定地知道出错的原因,不再需要为其添加null的检查,因为null的检查只会掩盖问题,并未真正地修复问题。
Insurance公司必须有个名字,所以,如果你遇到一个公司没有名称,你需要调查自己的数据出了什么问题,而不应该再添加一段代码,将这个问题隐藏。

在你的代码中始终如一地使用Optional,能非常清晰地界定出变量值的缺失是结构上的问题,还是你算法上的缺陷,抑或是你数据中的问题。

最后,还需要特别强调,引入Optional类的意图并非要消除每一个null引用。与此相反,它的目标是帮助你更好地设计出普适的API,让程序员看到方法签名,就能了解它是否接受一个Optional的值。这种强制会让你更积极地将变量从Optional中解放出来,直面缺失的变量值。

参考

《Java8 实战》

标签: none

入群须知:

凡是加入我群者,皆要严守群规,每周六、日是群规反思日。群规的要义有三点∶

(1)坚持系统化的学习方式,由量变到质变。仅仅解决工作中的问题,并不叫系统化的学习。

(2)坚持以价值为导向的学习方式,扔掉低价值知识[配置、调参、安装],聚焦高价值知识[结构、算法、优化],推动量变到质变的进程。

添加新评论