ITEM 49: CHECK PARAMETERS FOR VALIDITY
大多数方法和构造函数对传入参数的值有一些限制。例如,索引值必须是非负的,对象引用必须是非空的,这种情况并不少见。您应该清楚地记录所有这些限制,并在方法体的开头进行检查。这是一般原则的一个特例,即您应该尝试在错误发生后尽快检测它们。如果不这样做,就不太可能检测到错误,而且一旦检测到错误,就很难确定错误的来源。
如果一个无效的参数值被传递给一个方法,并且该方法在执行之前检查它的参数,那么它将快速而干净地失败,并出现一个适当的异常。如果方法未能检查其参数,可能会发生以下几件事情。该方法可能会在处理过程中出现令人困惑的异常而失败。更糟糕的是,该方法可能会正常返回,但会默默地计算错误的结果。最糟糕的是,该方法可能会正常返回,但会使某个对象处于折衷状态,从而在将来某个不确定的时间在代码中某个不相关的点上导致错误。换句话说,如果未能验证参数,则可能导致破坏故障原子性(item 76)。
对于 public 和 protected 方法,使用 Javadoc @throw 标记来记录如果违反了对参数值的限制将会引发的异常(item 74)。通常,生成的异常是 IllegalArgumentException、IndexOutOfBoundsException 或 NullPointerException (item 72)。一旦您记录了方法参数上的限制,并且记录了如果违反这些限制将引发的异常,那么执行这些限制就很简单了。这里有一个典型的例子:
/**
* Returns a BigInteger whose value is (this mod m). This method
* differs from the remainder method in that it always returns a
* non-negative BigInteger. *
* @param m the modulus, which must be positive
* @return this mod m
* @throws ArithmeticException if m is less than or equal to 0
*/
public BigInteger mod(BigInteger m) {
if (m.signum() <= 0)
throw new ArithmeticException("Modulus <= 0: " + m);
... // Do the computation
}
请注意,doc 注释并没有说 “mod在m为null时抛出NullPointerException”,尽管该方法确实是这样做的,这是调用 m.signum() 的副产品。这个异常记录在类级别的 BigInteger 类的 doc 注释中。类级注释适用于类的所有公共方法中的所有参数。这是避免在每个方法上分别记录每个NullPointerException 的混乱的好方法。它可以与 @Nullable 或类似的注释结合使用,以指示某个特定参数可能为 null,但这种做法并不标准,为此使用了多个注释。
在Java 7中添加的Objects.requireNonNull 方法非常灵活方便,因此没有必要再手动执行null 检查。如果愿意,您可以指定自己的异常详细信息。该方法返回它的输入,所以你可以执行一个空检查,同时你使用一个值:
// Inline use of Java's null-checking facility
this.strategy = Objects.requireNonNull(strategy, "strategy");
您还可以忽略返回值并使用对象。 Objects.requireNonNull 是一个独立的null检查,满足您的需要。
在 Java 9 中,一个范围检查功能被添加到 Java.util.objects 中。这个工具由三个方法组成:checkFromIndexSize、checkFromToIndex 和 checkIndex。这个工具不如空检查方法灵活。它不允许您指定自己的异常详细信息,而且它只用于列表和数组索引。它不处理封闭范围(其中包含它们的两个端点)。但如果它能满足你的需要,那就很方便了。
对于未导出的方法,作为包的作者,您可以控制调用该方法的环境,因此您可以并且应该确保只传递有效的参数值。因此,非公共方法可以使用断言检查它们的参数,如下所示:
// Private helper function for a recursive sort
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert length >= 0 && length <= a.length - offset;
... // Do the computation
}
本质上,这些断言是断言的条件将为真,而不管其客户端如何使用封装的包。与常规的有效性检查不同,如果断言失败,则会抛出 AssertionError。与普通的有效性检查不同,它们没有效果,而且基本上没有成本,除非您启用它们,这是通过向 java 命令传递 -ea(或 -enableassertion)标志来实现的。有关断言的更多信息,请参阅教程 [Asserts]。
特别重要的是,要检查方法没有使用但存储起来供以后使用的参数的有效性。例如,考虑第101页中的静态工厂方法,它接受 int array 并返回该数组的列表视图。如果客户机传递 null,该方法将抛出 NullPointerException,因为该方法有一个显式的检查(调用 object.requirenonnull)。如果忽略了检查,该方法将返回对新创建的 Listinstance 的引用,该引用将在客户端试图使用它时抛出 NullPointerException。到那时,列表实例的来源可能很难确定,这可能会使调试任务变得非常复杂。
构造函数代表了一种特殊的情况,即您应该检查要存储起来供以后使用的参数的有效性。检查构造函数参数的有效性对于防止构造违反其类不变量的对象是至关重要的。
在执行方法的计算之前,应该显式地检查方法的参数,这条规则也有例外。一个重要的例外是有效性检查是昂贵的或不切实际的,并且检查是在执行计算的过程中隐式执行的。例如,考虑一个对对象列表进行排序的方法,例如 Collections.sort(list)。
列表中的所有对象必须是相互可比的。在对列表排序的过程中,列表中的每个对象都将与列表中的其他对象进行比较。如果对象不是相互可比的,其中一个比较将抛出 ClassCastException,这正是 sort 方法应该做的。因此,事先检查列表中的元素是否具有可比性是没有意义的。但是,请注意,不加选择地依赖隐式有效性检查可能会导致失败的原子性损失(item 76)。
有时,计算隐式执行所需的有效性检查,但如果检查失败则抛出错误的异常。换句话说,由于无效的参数值而导致计算自然抛出的异常与方法记录为抛出的异常不匹配。在这种情况下,您应该使用 item 73 中描述的异常翻译习语来将自然异常转换为正确的异常。
不要从这一项推断对参数的任意限制是一件好事。相反,你应该把方法设计得既通用又实用。对参数的限制越少越好,假设该方法可以对它所接受的所有参数值进行合理的处理。然而,通常一些限制是实现抽象的固有的。
总而言之,每次编写方法或构造函数时,都应该考虑其参数存在哪些限制。您应该记录这些限制,并在方法主体的开头使用显式检查来强制执行它们。养成这样做的习惯很重要。它所包含的少量工作将在有效性检查第一次失败时连本带利地偿还。