首页 > 学院 > 开发设计 > 正文

[Effective Java]第七章 方法

2019-11-14 22:44:54
字体:
来源:转载
供稿:网友
[Effective java]第七章 方法声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.VEVb.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将追究法律责任!原文链接:http://www.VEVb.com/jiangzhengjun/p/4255671.html 第七章 方法38、 检查参数的有效性

绝大多数方法和构造器对于传递给它们的参数值都会有某些限制。例如,索引值必须是非负的,对象引用不能为null等,这些都是常见的。你应该在文档中清楚地指明所有这些限制,并且在方法体的开头处检查参数,以强制施加这些限制。

应该在方法和构造器体前进行了参数的有效性检查,并且及时向外抛出适当的异常。如果方法没有检查它的参数,就有可能发生几种情形。该方法可能在处理过程中失败,并且产生令人费解的异常,更有可能,该方法可以正常返回,但是会悄悄地计算出错误的结果。

对于公有的方法,要用JavaDoc的@throws标签(tag)在文档中说明违反参数值限制时会抛出的异常(见第62条),这样的异常通常为IllegalArgumentException、IndexOutOfBoundsException或NullPionterException(见第60条),下面是一个例子:

/**

* ...

*@paramm m为系数,必须是正数

*@return this mod m

*@throws如果m小于等于0时抛出ArithmeticException

*/

publicBigInteger mod(BigInteger m) {

if(m.signum() <= 0) {

thrownewArithmeticException("Modulus <= 0:" + m);

}

...// do the computations

}

对于那此非公有方法,你可以控制这个方法在哪些情况下被调用,因此你可以,也应该确保只将有效的参数值传递进来。因此,非公有的方法通常应该使用断言来检查它们的参数,具体做法如下:

PRivatestaticvoidsort(longa[],intoffset,intlength) {

asserta !=null;

assertoffset >= 0 && offset <= a.length;

assertlength >= 0 && length <= a.length - offset;

// ... do the computation

}

断言就是被断言的条件将会为真,否则将会抛出AssertionError。不同于一般的有效性检查,如果它们没有起到作用,本质上也不会有成本开销,除非通过将–ea(或者-enableassertions)开关传递给Java解释器,来启用它们。

对于有些参数,方法本身没有用到,却被保存起来供以后使用,则检验这类参数的有效性尤其重要,因为将来在使用时抛出异常时要想找到这个参数的来源就非常困难了,所以这类参数更应该先检查,这个规则也适用于构造函数。

不是所有的参数都就应该在使用前检查他的有效性,因为有的检查是要代价的,或根本是不切实际的,而且有效性检查本身就可以在计算过程中完成。例如,Collections.sort(List)对集合排序的方法,集合中的每个元素都实现了Comparable接口,否则在计算时会抛出ClassCastException,这正是sort方法所应该做的事情,因此,提前检测是否实现了该比较接口是没有多大意义的,再说多遍历一次也是消耗性能的,但此时我们先不进行参数有效性检测,则无效的参数值会导致计算过程抛出异常,而这种异常与文档中标明这个方法抛出的异常不符,在这种情况下,我们应该转换异常,将计算过程中抛出的异常转换为正确的异常。

当然,不是要求所有的参数都要求检查的,有些参数在实际应用中不会产生非法值或在我们需范围内不会有其他值,则就不需要进行检测。

39、 必要时进行保护性拷贝

保护性拷贝的实例:

publicfinalclassPeriod {

privatefinalDate start;

privatefinalDate end;

publicPeriod(Date start, Date end) {

this.start =newDate(start.getTime());

this.end =newDate(end.getTime());

//开始时间一定不能大于结束时间

if(this.start.compareTo(this.end) > 0)

thrownewIllegalArgumentException(start + " after " + end);

}

publicDate start() {

return(Date)start.clone();

//returnnewDate(start.getTime());

}

publicDate end() {

return(Date)end.clone();

//returnnewDate(end.getTime());

}

}

这里两个地方都需要进行保护性拷贝,第一个是进参的地方,这里是构造器;第二个地方是传出去的地方,这里为start与end方法,如果丢掉一个地方,都有可能打破上面约束条件“开始时间一定不大于结束时间”。注,在构造器中,保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象,这是一定要这么做的,因为如果将检测放在了最前面,则当检测满足条件后另一线程改变了原始对象参数的值,此进检测实质上已无效,所以这里与第38条并不是矛盾的。

同时请注意,构造器中我们没有用Date的clone方法来进行保护性拷贝。因为Date是非final类,不能保证传进来的一点是Date类型对象:它有可能是专门出于恶意的目的而设计的不可信子类的实例,这样我们调用clone方法时实质上不是调用Date上面的clone方法,而是恶意子对象上的,这样在克隆时子类可以在每个克隆实例被创建的时候,把指向该实例的引用记录到一个私有的静态列表中,并且允许攻击者访问这个列表,这将使得攻击者可以自由地控制所有的实例。所以,对于传进的参数实例我不要使用clone方法进行保护拷贝,而是直接使用new的创建方式来拷贝。

然而,对于访问方法,与构造器不同,在进行保护拷贝时候是允许使用clone方法的。之所以可以,是因为我们知道,Period内部的Date对象的类是java.util.Date,而不可能是其他某个潜在的不可信子类。

参数的保护性拷贝并不仅仅针对不可变类。每当编写方法或者构造器时,如果它要允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能容忍对象进入数据结构之后发生变化,如果不允许,就必须对该对象进行保护性拷贝,以防止相互影响。

在内部组件返回给客户端之前,对它们进行保护性拷贝也是同样的道理。不管类是否为不可变的,在把一个指向内部可变组件的引用返回给客户端之前,也应该考虑是否进行拷贝。

记住长度非零的数组总是可变的。因引,在把内部数组返回给客户端之前,应该总要进行保护性拷贝。另一种解决方案是,给客户端返回该数组的不可变视图,这两种方法在第13条中已演示过了。

保护性拷贝可能会带来相关的性能损失,如果类信任它的调用都不会修改内部的组件,可能因为类及其客户端都是同一个包的双方,那么不进行保护性拷贝也是可以的。在这种情况下,类的文档必须清楚地说明,调用者绝不能修改受影响的参数或者返回值。

总之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件。如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改这些组件,就可以在文档中指明不能修改这些受到影响的组件,以此来代替保护性拷贝。

40、 谨慎设计方法签名

1、谨慎地选择方法的名称。遵循标准命名习惯,风格统一、大众认可的相一致的名称。设计时可以参考类库。

2、不要过于追求提供便利的方法。每个方法都应该尽其所能。方法太多会使类难以学习、使用、文档化、测试和维护。对于接口,更是这样,方法太多会使用接口实现者和接口用户的工作变得复杂起来。对于类和接口所支持的每个动作,都提供一个功能齐全的方法。只有当一项操作被经常用到时,考虑为它提供快捷的方式。

3、避免过长的参数列表。目标是四个参数或者更少。相同类型的长参数序列格外有害,如果不小心弄错了参数顺序时,他们的程序仍然可以编译和运行。有三种方法可以缩短过长的参数列表。第一种是把方法分解成多个方法,每个方法只需要这些参数的一个子集,即将多功能分解成多个单一的小功能。第二种方法是创建辅助类,如果使用FromBean封装了页面上的所有参数然后传到Action。第三种是结合了前两种方法特征,从对象构造到方法调用都采用Builder模式(参见第2条),如果方法有多个参数,其中有些又是可选的,最好定义一个对象来表示所有参数,并允许客户端在这个对象上进行多次“setter”调用,每次调用都设置一个参数或设置一个较小的集合,一旦设置了所需要的参数,客户端就调用这个对象的“执行(execute)”方法,它对参数进行最终的有效性检查,并执行实际的计算。

对于参数传递类型,我们要优先使用接口而不是类(请见第52条),只要有适当的接口可用来定义参数,就优先使用这个接口,而不是这个接口实现。我们没有理由在编写方法时使用HashMap类来作为输入,相反,应当使用Map接口作为参数类型,这使你可以传进各种Map的实现,如果碰巧输入的数据是以其他形式存在,使用具体类类型作为参数时就会导致不必要的转换操作。

对于boolean参数,要优先使用两个元素的枚举类型,这样便于以后第种选择的加入,这样不必要再添加另一个方法。

41、 慎用重载

publicclassCollectionClassifier {

publicstaticString classify(Set<?> s) {

return"Set";

}

publicstaticString classify(List<?> lst) {

return"List";

}

publicstaticString classify(Collection<?> c) {

return"Unknown Collection";

}

publicstaticvoidmain(String[] args) {

Collection<?>[] collections = {newHashSet<String>(),

newArrayList<BigInteger>(),newHashMap<String, String>().values() };

for(Collection<?> c : collections)

System.out.println(classify(c));

}

}

Word-spacing: 0px; text-transform: none; word-break: normal; margin: 0cm 0cm 0

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表