一、检查参数的有效性
极大多数方法和构造函数都会对于传递给它们的参数值有某些限制。
对于公有的方法,使用Javadoc @throws标签(tag)可以使文档中记录下“一旦针对参数值的限制被违反之后将会被抛出的异常”。典型情况下, 这样的异常为IllegalArgumentException、IndexOutOfBoundException或者NullPointException。看一个例子:
/** * @param m the modulus,which must be positive. * @return this mod m. * @throws ArithmeticException if m<=0. */public BigInteger mod(BigInteger m){ if(m.signum()<=0) throw new ArithmeticException("Modulus not positive"); ...//Do the computation}
二、需要时使用保护性拷贝
Java程序设计语言用起来如此愉悦的一个原因是,它是一门安全的语言(safe language)。这意味着无需专门手段,它对应缓冲区溢出、数组越界、非法指针以及其他的内存破坏错误自动免疫,而这些错误却困扰着诸如C和C++这样的不安全语言。
例如,下面是表达一段不可变的时间周期:
//Broken "immutable" time period classpublic final class Period{ private final Date start; private final Date end; /** * @param start the beginning of the period. * @param end the end of the period;must not precede start. * @throws IllegalArgumentException if start is after end. * @throws NullPointException if start or end is null. */ public Period(Date start, Date end){ if(start.compareTo(end) > ) throw new IllegalArgumentException(start+" after "+end); this.start = start; this.end = end; } public Date start(){ return start; } public Date end(){ return end; } ...//Remainder omitted}
上面的Date类本身是可变的,就可以知道这个约束条件很容易被违反:
//Attack the internals of a Period instanceDate start = new Date();Date end = new Date();Period p = new Period(start,end);end.setYear(78); //Modifies internals of p!
为了保护Period实例的内部信息避免受到这种攻击,对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是必要的,并且使用拷贝之后的对象作为Period实例的组件,而不使用原始的对象。代码改写如下:
//Repaired constructor = makes defensive copies of parameterspublic Period(Date start,Date end){ this.start = new Date(start.getTime()); this.end = new Date(end.getTime()); if(this.start.compareTo(this.end)>0) throw new IllegalArgumentException(start +" after "+ end);}
注意,保护性拷贝动作时在检查参数的有效性之前进行的,并且有效性检查时针对拷贝之后的对象,而不是原始的对象。虽然这样看起来有点不太自然,但这是必要的。这样做可以避免“脆弱性窗口”中另外一个线程会改变原始的参数对象,这里脆弱性窗口是指从参数检查开始,一直到参数对象被拷贝之间的一段时间窗。
//Second attack on the internals of a Period instanceDate start = new Date();Date end = new Date();Period p = new Period(start,end);p.end().setYear(78);//modifies internals of p!
为了防御第二种攻击,只需修改这两个访问方法,使它返回可变内部域的保护性拷贝即可:
//Repaired accessors - make defensive copies of internal fieldspublic Date start(){ return (Date)start.clone();}public Date end(){ return (Date)end.clone();}
采用了新的构造函数和新的访问方法之后,Period成为真正的非可变类。
三、谨慎设计方法的原型
谨慎选择方法的名字。方法的名字应该总是遵循标准的命名习惯。不要过于追求提供便利的方法。避免长长的参数列表。通常,三个参数应该被看做实践中最大值,而且参数越少越好。类型相同的长参数序列尤其有害。当弄错了参数顺序的时候,他们的程序仍然可以编译和运行。
有两项技术可以缩短太长的参数列表。a、把一个方法分解成多个方法,每一个方法只要求这些参数的一个子集。b、缩短长参数列表的技术是创建辅助类(helper class),用来保存参数的聚集(aggregate),这些辅助类往往是静态成员类。对于参数类型,优先使用接口而不是类。无论什么时候,只要存在可用来定义参数的适当接口,就优先使用这个接口,而不是实现该接口的类。例如,没有理由在编写一个方法时,使用Hashtable作为输入,相反,应该使用Map。这使得你可以传入一个Hashtable、HashMap、TreeMap、TreeMap的子映射表(submap),或者任何有待于将来编写的Map实现。如果使用的是一个类而不是一个接口,则限制了只能传入一个特定的实现,如果碰巧输入的数据时以其他形式存在的话,则会导致不必要的、可能非常昂贵的拷贝操作。谨慎的使用函数对象。 创建函数对象最容易的方法莫过于使用匿名类,但是这样会带来语法上的混乱。四、谨慎地使用重载
下面的一个意图良好的集合分类器,根据一个集合(collection)是Set、List,或是其他的集合类型,对它进行分类:
public class CollectionClassifier { public static String classify(Set s){ return "Set"; } public static String classify(List l){ return "List"; } public static String classify(Collection c){ return "Unknown Collection"; } public static void main(String args[]){ Collection[] tests = new Collection[]{ new HashSet(), //A set new ArrayList(), //A arraylist new HashMap().values() //neither set or list }; for(int i=0;i
结果:
Unknown CollectionUnknown CollectionUnknown Collection结果为什么不是“Set”,“List”以及“Unknown Collection”呢?是因为classify方法被重载(overloading)了,而到底调用哪个重载(overloading)方法时编译时刻作出决定的。由于上面例子的for循环的全部三次迭代,参数编译时类型都是Collection,每次迭代的运行时类型是不同的,但这并不影响对重载方法的选择。因为该参数的编译时类型为Collection,所以,唯一合适的重载方法是第三个:classify(Collection),在循环的每次迭代中,都会调用这个重载方法。
这个程序的行为是违反了直觉的,因为对于重载方法(overloaded method)的选择是静态的,而对于被改写的方法(overridden method)的选择是动态的。对于被改写的方法,选择正确的版本是在运行时刻进行的,选择的依据是被调用方法所在对象的运行时类型。重写的方法是发生在子类继承时,当子类申明的方法与其父类具有相同的原型时。如下面的例子:
public class A { String name() { return "A"; }}public class B extends A{ String name(){ return "B"; }}public class C extends A { String name(){ return "C"; }}public class Overriding { public static void main(String[] args) { A[] tests = new A[]{ new A(),new B(),new C()}; for(int i = 0;i
结果:
ABC一个安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。
“你能够重载方法”并不意味着“你应该重载方法”。一般地,对于多个相同参数数目的方法来说,你应该尽量避免重载方法。在某些情况下,特别是涉及到构造函数的时候,遵循这条建议也许是不可能的。但至少应该避免这种情形:同一组参数只需经过类型转换就可以传递给不同的重载方法。
四、返回零长度的数组
像下面这样的方法并不少见:
public Cheese[] getCheeses(){ if(cheesesInStock.size()==0) return null; ...}
有观点认为,返回null比零长度数组更好,因为它避免了分配数组所需要的开销,这种观点是站不住脚的,原因有两点:
第一,在这个层次上担心性能问题是不明智的,除非分析表明这个方法正是造成性能问题的真正源头;第二,对于不返回任何元素的调用,每次都返回同一个零长度数组是有可能的,因为零长度数组是非可变的,而非可变对象有可能被自由地共享。五、为所有导出的API元素编写文档注释
Java语言环境提供了一个javadoc的实用工具,从而使编写API文档这项任务变得容易。这个工具可以根据源代码自动产生API文档,它利用了源代码中特殊格式的文档注释(documentation comment,通常被写作doc comment)。
为了正确地编写API文档,你必须在每一个被导出的类、接口、构造函数、方法和域声明之前增加一个文档注释。
每一个方法的文档注释应该简洁地描述出它和客户之间的约定。这个约定应该说明了这个方法做了什么,而不是说明它是如何完成这项工作的。文档注释应该列举出这个方法所有的前提条件(precondition)和后置条件(postcondition),所谓前提条件是指为了使客户能够调用这个方法,而必须要满足的条件;所谓后置条件是指在调用成功完成之后,哪些条件必须要满足。典型情况下,前提条件有@throws标签所隐含描述的;每一个未被检查的异常都对于着一个被违背的前提条件。同样地,你也可以在一些受影响的参数的@param标记中指定前提条件。
除了前提条件(precondition)和后置条件(postcondition)之外,还应该描述其副作用(side effect),所谓副作用是指系统状态中一个可观察的变化,它不是为了获得后置条件而要求的变化。例如,如果一个方法启动了一个后台线程,那么文档中应该说明这一点。
@throws标签之后的文字应该包含单词“if”(如果),紧接着实一个名称短语,它描述了这个异常将在什么样的条件下会被抛出来。偶尔情况下用算术表达式来代替名称短语。如下摘自List接口的文档注释演示了所有这些习惯:
/** * Returns the element at the specified position in this list. * * @param index index of element to return;must be nonnegative and less than the size of this list. * @return the element at the specified position in this list. * @throws IndexOutOfBoundsException if the index is out of range * /
文档注释格式:
第一句话是注释所属元素的概要描述(summary description)。概要描述必须独立地描述目标实体的功能。为了避免混淆,同一个类或者接口中,不应该存在两个成员或者构造函数具有同样地概要描述。特别要注意重载的情形,特别要注意重载的情形,在这种情况下,往往自然地在描述中使用同样地第一句话。
小心,在文档注释的第一句话内部不要包括句号。如果你包括了句号,则它会终止整个概要描述。例如,一个以“A college degree,such as B.S.,M.S.,or Ph.D"开头的文档注释,它的概要描述为”A college degree,such as B."避免这种问题最容易的方法是,在概要描述中不要使用缩写和十进制小时,然而,在概要描述中使用句号也是可能地,你只需用句号的数字编码形式(numeric encoding)“."来代替它,虽然这样做可以工作,但不会生成漂亮的源代码。