使用Null对象替代引用是否为空判断
编程语言中最常见运行时异常非NullPointerException莫属,只要程序依赖于外部的输入数据,比如说http请求传递的查询字符串参数、关系数据库连接、磁盘文件读取,空引用异常就无法避免。通常,程序需要满足某些条件才能正常的往下执行,假如这些条件依赖外部输入数据,而这些外部输入的数据肯定无法保证百分百不出错,比如说网络连接失败、数据库用户名密码错误等,当程序被这些节外生枝的障碍打断时,空引用异常就极有可能被引发。
比如说,原本我们调用一个方法,这个方法会执行连接数据库操作并返回一个数据库连接对象。然而,由于某种原因导致连接失败,这个方法并没有照常返回数据库连接对象而是返回一个null值,当我们使用对象时假如不进行是否为空检测,程序就会抛出NullPointerException,但是假如进行检测的话代码又会变得极其丑陋,没有程序员喜欢自己的代码中到处都是if(connection == null)这样的条件判断。
而且这种对象是否为空的判断还会传播,在一系列函数调用的过程中,其中某一个调用返回一个null值, 这个函数调用栈中所有的调用都有可能受到波及,直到最外层的调用。这些函数中会出现很多是否为空的判断,严重影响代码的美观程度、可读性,甚至还增加了出BUG的几率。
空引用问题是永远无法避免的, 除非从语言层面进行解决, 现在一些现代的新语言的设计已经引入避免此问题的机制。但是一些年纪较大的语言, 比如说Java,只能通过一些代码编写技巧来尽量弱化空引用带来的问题。「使用Null对象代替是否为空判断」是一种流行的解决此问题的技巧。
public class Main {
public static void main(String[] args) {
getWorstLanguage(false).toString();
getBestLanguage(false).toString();
}
public static String getWorstLanguage( boolean exists) {
if(exists ) {
return "Java";
} else {
return "";
}
}
public static String getBestLanguage( boolean exists ) {
if( exists ) {
return "PHP";
} else {
return null;
}
}
}
main方法中第二行代码执行会抛出空引用异常。 其实两个方法的if条件都没有被满足,然而它们一个返回长度为0的空字符串,一个返回null, 空字符串虽然没有实际意义,但却并非是空引用, 因此在其上执行操作不会抛出空引用异常,代码也是相对安全的。
想要避免getBestLanguage方法使用时有可能会引发的异常,一般都会这么做
String result = getBestLanguage(false);
if (result != null) {
result.toString();
}
显然这并不如getWorstLanguage方法返回空字符串的方式来的优雅。如果你对字符串的长度有依赖,可以以
if (result.length() == 0 ) {}
这种方式进行判断,至少它没有出现异常的危险。
同样,在方法返回值为其它对象类型的时候也可以借鉴并扩展这种思路。
public class Customer {
String _name;
public Customer() { }
public Customer(String name) {
_name = name;
}
public String GetName() {
return _name;
}
}
public class Site {
Customer _customer;
public Site() {
}
public Site(Customer customer) {
_customer = customer;
}
public Customer GetCustomer() {
return _customer;
}
}
有Customer 和 Site 两个类,Site类的GetCustomer会返回一个Customer对象,但假如实例化Site对象时使用无参数构造函数,GetCustomer将返回一个空引用。
Site site = new Site();
Customer customer = site.GetCustomer();
String name = customer == null ? "guest" : customer.GetName();
System.out.println(name);
像这种方式使用那两个类,在调用GetName方法时, 除非进行是否为空校验,否则程序会抛出空引用异常。 假如site对象和它的GetCustomer会被频繁的调用,将是难以忍受的,因为customer == null 这样的条件判断会充斥在项目代码的各处。
我们可以引入一个“空”对象来改善这个问题
public class Customer {
String _name;
public Customer() { }
public Customer(String name) {
_name = name;
}
public String GetName() {
return _name;
}
public boolean IsNull()
{
return false;
}
}
public class NullCustomer extends Customer {
public NullCustomer() {}
@Override
public boolean IsNull() {
return true;
}
@Override
public String GetName() {
return "guest";
}
}
public static class Site {
Customer _customer;
public Site() {
}
public Site(Customer customer) {
_customer = customer;
}
public Customer GetCustomer() {
return _customer == null ? new NullCustomer(): _customer;
}
}
Customer类添加了一个IsNull的实例方法。
Site 类的GetCustomer方法内部进行了_customer 成员是否为空的判断,这其实就是把原来在外面的空引用判断提取到了类的内部,把逻辑给封装了起来。
与此同时, 我们引入了NullCustomer类型,它继承至Customer,是一个Customer的特例,表示Site对象中_customer成员为空的情况,替代它非空时的行为, 这正如他的命名NullCustomer。
原本调用Site对象GetCustomer有可能返回的null值被NullCustomer类的实例所代替, 这样代码的外部可以放心的使用GetCustomer的返回值,不用再提心吊胆的生怕返回空值,也不用做是否为空的判断。
public static void main(String[] args) {
Site site = new Site();
Customer customer = site.GetCustomer();
String name = customer.GetName();
System.out.println(name);
}
如果要确定GetCustomer的返回值是否为空的情况,可用调用customer对象的IsNull方法
Customer customer = site.GetCustomer();
if(customer.IsNull()) {
}
因为不管是Customer 类还是NullCustomer都实现了这个方法, 并且Customer的IsNull方法返回的false, NullCustomer类重写了重Customer继承来的IsNull方法,并返回为true,这种利用多态来面向接口编程的方式,正好满足了我们的需求。
总而言之,引用“空”对象可以很好的解决空引用这个牛皮癣似的问题。然而,引入这个机制还需要跟代码的实际情况结合,假如某个对象为空的情况只出现有限的几次,那引入这种机制显得有些杀鸡用牛刀的味道了,使用是否为空判断反而更加轻松;当某个对象是否为空的判断频繁的出现在代码之中, 那么使用“空”对象来代替if判断才有实际的意义。