在软件开发过程中,经常会遇到系统崩溃或类似的错误,这些错误往往是由于令人沮丧的NullPointerException引起的。为了保护软件免受此类问题的影响并使其更安全,通常会在代码中添加检查,以防止调用null变量。这导致了一个简单的方法,如:
public String getProjectManagerName() {
return getProject().getManager().getName();
}
变成了这样:
public String getProjectManagerName() {
Project project = getProject();
if (project == null)
return "";
Manager manager = project.getManager();
if (manager == null)
return "";
return manager.getName();
}
这种方法在只需要在一两个地方使用时效果很好,但如果需要在所有软件中重复使用,它会通过添加不必要的代码,显著降低其可读性和可扩展性。此外,这种类型的空逻辑无法为新代码提供保护,因此如果程序员忘记包含它,相同的问题可能会再次发生。
这个问题非常重要,以至于在更现代的语言如Groovy中引入了安全引用操作符(?.)。使用这个构造,前面的方法可以安全地重写如下:
public String getProjectManagerName() {
return getProject()?.getManager()?.getName();
}
虽然Java语言没有提供安全引用操作符,但仍然可以通过使用所谓的空对象模式来实现相同的结果。根据Wikipedia的定义:
“空对象是一个具有定义的中性(“null”)行为的对象。”
换句话说,一个给定类的空对象与该类兼容(通过扩展它或实现一个公共接口),并在没有更有意义的行为的情况下提供默认的(“null”)行为。例如,前面代码片段中涉及的类Project和Manager的空对象版本可以如下实现:
public class NullProject extends Project {
public Manager getManager() {
return new NullManager();
}
}
public class NullManager extends Manager {
public String getName() {
return "";
}
}
请注意,空对象必须始终返回另一个空对象,以保持调用序列的安全。通过这种方式(假设getProject()方法在应该返回null时返回NullProject的实例),getProjectManagerName()的第一个版本再次变得安全,消除了第二个版本中引入的所有检查的需要。
在看来,尽管这种解决方案有效,并且允许编写更干净的代码,但它有一个主要缺陷:它迫使为领域模型中的每个类实现、维护和测试空对象版本。而且,它并没有以更微妙的方式为新代码提供保护:如果程序员在领域对象中添加了新方法,忘记了在其空对象版本中覆盖它,将面临NullPointerException的风险。
为了消除这些问题,最好通过代理动态生成空对象类,而不是静态实现它们。为了探索这个最后的解决方案,实现了一个名为NullObjectProxy的类,它附加在本文中,以及一个测试类,它更好地说明了如何使用它。这个代理的核心当然是它的invoke()方法,它定义了在拦截对空对象的调用时执行的操作。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("getNullClass") && (args == null || args.length == 0))
return clazz;
// 它将hashCode和equals方法委托给代理类
if (methodName.equals("hashCode") && (args == null || args.length == 0))
return clazz.hashCode();
if (methodName.equals("equals") && args.length == 1 && args[0] instanceof NullObject)
return ((NullObject)args[0]).getNullClass() == clazz;
// 它检查是否为此方法调用定义了特殊返回值
Method mockedMethod = getMockedMethod(method);
if (mockedMethod != null)
return mockedMethodValuesMap.get(mockedMethod);
Class> returnedType = method.getReturnType();
if (returnedType == Void.TYPE)
return null;
// 如果返回类型是接口
if (returnedType.isInterface())
return nullObjectOf(returnedType);
// 它检查是否为此返回类型定义了默认值
try {
return NullObjectProxy.class.getMethod("null" + returnedType.getSimpleName() + "Value").invoke(NullObjectProxy.class);
} catch (Exception e) { }
// 它尝试实例化给定返回类型的一个对象,通过调用其空构造函数(如果有的话)
try {
return returnedType.newInstance();
} catch (Exception e) { }
// 如果所有前面的策略都失败了,为了避免ClassCastException,它只能返回null
return null;
}
此类允许动态实例化一个代理,该代理为任何给定的接口实现空对象模式,通过将要模拟的接口的类传递给静态构造函数:
public static T nullObjectOf(Class clazz);
这个代理保证了任何调用序列的安全性,因为每个方法调用都返回(如果可能的话)另一个空对象,该对象反过来模拟方法本身返回的类型。通过这种方式,要使原始的非检查实现的getProjectManagerName()方法安全,只需从getProject()调用返回nullObjectOf(Project.class)即可。
如上所述,当被调用方法返回的声明类型不是接口时,空对象代理无法生成另一个空对象。在这种情况下,代理尝试返回一个有意义的值,应用两种策略。首先,它检查是否有与请求兼容的预定义默认返回值,特别是对于任何Number返回0,对于boolean返回false,对于char返回空格,对于String和Date分别返回空字符串和表示当前系统时间的Date。然后,如果没有上述情况适用,它尝试实例化给定返回类型的一个对象,通过调用其空构造函数(如果有的话)。最后,如果这两种策略都失败了,为了避免ClassCastException,它唯一能做的就是返回null。
setMockedMethod(Collection.class, "isEmpty", true);