Effective Java (方法)
三十八、检查参数的有效性:
?? ?? 绝大多数方法和构造器对于传递给它们的参数值都会有些限制。比如,索引值必须大于等于0,且不能超过其最大值,对象不能为null等。这样就可以在导致错误的源头将错误捕获,从而避免了该错误被延续到今后的某一时刻再被引发,这样就是加大了错误追查的难度。就如同编译期能够报出的错误总比在运行时才发现要更好一些。事实上,我们不仅仅需要在函数的内部开始出进行这些通用的参数有效性检查,还需要在函数的文档中给予明确的说明,如在参数非法的情况下,会抛出那些异常,或导致函数返回哪些错误值等,见如下代码示例:
1???? /**
?? ??? 是不是我们为所有的方法均需要做出这样的有效性检查呢?对于未被导出的方法,如包方法等,你可以控制这个方法将在哪些情况下被调用,因此这时可以使用断言来帮助进行参数的有效性检查,如:
1???? public final class Period {
????? 从表面上看,该类的实现确实对约束性的条件进行了验证,然而由于Date类本身是可变了,因此很容易违反这个约束,见如下代码:
6 }????? 需要说明的是,保护性拷贝是在坚持参数有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象的。这主要是为了避免在this.start = new Date(start.getTime())到if (start.compareTo(end) > 0)这个时间窗口内,参数start和end可能会被其他线程修改。
?? ?? 现在构造函数已经安全了,后面我们需要用同样的方式继续修改另外两个对象访问函数。
1???? public class CollectionClassfier {
????? 这里你可能会期望程序打印出
?? ?? //Set
?? ?? //List
?? ?? //Unknown Collection
?? ?? 然而实际上却不是这样,输出的结果是3个"Unknown Collection"。为什么会是这样呢?因为函数重载后,需要调用哪个函数是在编译期决定的,这不同于多态的运行时动态绑定。针对此种情形,该条目给出了一个修正的方法,如下:
????? 和override不同,重载机制不会像override那样规范,并且每次都能得到期望的结果。因此在使用时需要非常谨慎,否则一旦出了问题,就会需要更多的时间去调试。该条目给出以下几种尽量不要使用重载的情形:
?? ?? 1.?? ?函数的参数中包含可变参数;
? ? ? 2.?? ?当函数参数数目相同时,你无法准确的确定哪一个方法该被调用时;
? ? ? 3.?? ?在Java 1.5 之后,需要对自动装箱机制保持警惕。
?? ?? 我们先简单说一下第二种情形。比如两个重载函数均有一个参数,其中一个是整型,另一个是Collection<?>,对于这种情况,int和Collection<?>之间没有任何关联,也无法在两者之间做任何的类型转换,否则将会抛出ClassCastException的异常,因此对于这种函数重载,我们是可以准确确定的。反之,如果两个参数分别是int和short,他们之间的差异就不是这么明显。
? ? ? 对于第三种情形,该条目给出了一个非常典型的用例代码,如下:
1???? public class SetList {
?? ?? 在执行该段代码前,我们期望的结果是Set和List集合中大于等于的元素均被移除出容器,然而在执行后却发现事实并非如此,其结果为:
? ? ? [-3,-2,-1] [-2,0,2]
? ? ? 这个结果和我们的期望还是有很大差异的,为什么Set中的元素是正确的,而List则不是,是什么导致了这一结果的发生呢?下面给出具体的解释:
? ? ? 1. s.remove(i)调用的是Set中的remove(E),这里的E表示Integer,Java的编译器会将i自动装箱到Integer中,因此我们得到了想要的结果。
? ? ? 2. l.remove(i)实际调用的是List中的remove(int index)重载方法,而该方法的行为是删除集合中指定索引的元素。这里分别对应第0个,第1个和第2个。
? ? ? 为了解决这个问题,我们需要让List明确的知道,我们需要调用的是remove(E)重载函数,而不是其他的,这样我们就需要对原有代码进行如下的修改:
1???? public class SetList {
? ? ? 该条目还介绍了一种实现函数重载,同时又尽可能避免上述错误发生的方式。即其中的一个重载函数,在其内部通过一定的转换逻辑转换之后,再通过转换后的参数类型调用其他的重载函数,从而确保即便使用者在使用过程中出现重载误用的情况,也因两者可以得到相同的结果而规避了潜在错误的发生。
四十二、慎用可变参数:
? ? ? 可变参数方法接受0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法,如:
1???? static int min(int...args) {
? ? ? 对于上面的代码主要存在两个问题,一是如果调用者没有传递参数是,该函数将会在运行时抛出异常,而不是在编译期报错。另一个问题是这样的写法也是非常不美观的,函数内部必须做参数的数量验证,不仅如此,这也影响了效率。将编译期可以完成的事情推到了运行期。下面提供了一种较好的修改方式,如下:
1???? static int min(int firstArg,int...remainingArgs) {
? ? ? 由此可见,当你真正需要让一个方法带有不定数量的参数时,可变参数就非常有效。
? ? ? 有的时候在重视性能的情况下,使用可变参数机制要特别小心。可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果确定确实无法承受这一成本,但又需要可变参数的灵活性,还有一种模式可以弥补这一不足。假设确定对某个方法95%的调用会有3个或者更少的参数,就声明该方法的5个重载,每个重载方法带有0个至3个普通参数,当参数的数目超过3个时,就使用一个可变参数方法:
1???? public class CheesesShop {
?? ?? 从以上代码可以看出,当没有Cheese的时候,getCheeses()函数返回一种特例情况null。这样做的结果会使所有的调用代码在使用前均需对返回值数组做null的判断,如下:
1 public void testGetCheeses(CheesesShop shop) {2 Cheese[] cheeses = shop.getCheeses();3 if (cheese != null && Array.asList(cheeses).contains(Cheese.STILTON))4 System.out.println("Jolly good, just the thing.");5 }? ? ? 对于一个返回null而不是零长度数组或者集合的方法,几乎每次用到该方法时都需要这种曲折的处理方式。很显然,这样是比较容易出错的。如果我们使getCheeses()函数在没有Cheese的时候不再返回null,而是返回一个零长度的数组,那么我的调用代码将会变得更加简洁,如下:
1 public void testGetCheeses2(CheesesShop shop) {2 if (Array.asList(shop.getCheeses()).contains(Cheese.STILTON))3 System.out.println("Jolly good, just the thing.");4 }? ? ? 相比于数组,集合亦是如此。