第三十八条、检查参数的有效性
如果传递无效的参数值给方法,这个方法在执行之前应该先对参数进行检查,那么它很快就会失败,并清楚地出现适当的异常。如果不做参数检查,则会出现很多不可控的错误。
-
对于公有的方法,要用Javadoc的
@throws
标签在文档中说违反参数值限制时会抛出的异常。通常为IllegalArgumentException
、IndexOutOfBoundsException
或者NullPointerException
。一旦在文档中记录了对于方法参数的限制,并且记录了一旦违反这些限制将要抛出的异常,强加这些限制就是非常简单的事了。/** * @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("Moduus <=0" + m); ... }
-
对于未被导出的方法,作为包的创建者,你可以控制这个方法将在哪些情况下被调用,因此你可以,也应该确保只将有效的参数值传递进来。非公有的方法通常应该使用断言assertion来检查它们的参数。
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; ... }
本质上来讲,这些断言是在声称被断言的条件将会为真,无论外围包的客户端如何使用它。不同于一般的有效性检查,断言如果失败,将会跑出
AssertionError
。 对于有些参数,方法本身没有用到,却被保存起来供以后使用。构造器Constructor正是代表这一原则的特殊类型。检查构造器的参数有效性是非常重要的。
-
有一个很重要的例外:在有些情况下有效性的检查工作非常昂贵,或者根本是不切实际的,而且有效性的检查已隐含在计算过程中完成.
例如:考虑一个为对象列表排序的方法:Collection.sort(List),列表中的所有对象都必须是可以互相比较的。这些是sort函数应该做的事情,因此提前检查的意义不大。然而,不加选择地使用这些方法将会导致失去失败原子型。
有时候,某些计算会隐式地执行必要的有效性检查,如果检查不成功,会抛出错误的异常。这与文档中标明的这个方法将要抛出的异常不符,这种情况下,应该使用异常转译技术,将计算过程中抛出的异常转换成正确的异常。 总结:每当编写方法或者构造器的时候,应该先考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显式的检查来实施这些限制。养成这样的习惯是非常重要的。
第三十九条、必要时进行保护性拷贝
对于构造器的每个可变参数进行保护性拷贝是必要的。保护性拷贝是在检查参数有效性之前进行的,并且有效性检查是针对拷贝后的对象。
this.start = new Date(start.getTime());
针对访问方法提供了对其内部可变成员的访问能力,则需要修改访问方法,使之返回可变内部域的保护性拷贝即可。
return new Date(start.getTime());
-
参数的保护性拷贝不仅仅针对不可变类:
每当编写方法或者构造器时,如果它允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是可变的。如果是,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果答案是否定的,就必须对该对象进行保护性拷贝,并且让拷贝之后的对象而不是原始数据对象进入到数据结构中。
在内部组件被返回给客户端之前,对它们进行保护性拷贝也是同样的道理。不管类是否是可变的,在把一个指向内部可变组件的引用返回给客户端之前,应该加倍认真考虑。【记住:长度非零的数组总是可变的,因此,把内部数组返回给客户端之前,应该总要进行保护性拷贝】 真正的启示在于:尽量使用不可变的对象作为对象内部的组件。
如果拷贝的成本得到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。
第四十条、谨慎设计方法签名
若干API设计技巧:
谨慎地选择方法的名称:方法的名称应该遵循标准的命名习惯,易于理解,同一个包中风格一致;选择大众认可的名字。
不要过于追求提供便利的方法:每个方法都应该尽其所能,对于类和接口所支持的每个动作,都提供一个功能齐全的方法。避免方法太多。
-
避免过长的参数列表:目标是四个参数或更少。相同类型的长参数序列格外有害,API用户不仅无法记住参数的顺序,而且一旦弄错了顺序,编译不会报错。
缩短长参数列表的方法:
- 把方法分解成多个方法,每个方法需要这些参数的一个子集。
- 创建辅助类用来保存参数的分组。这些辅助类一般是静态成员类。
- 从对象构建到方法调用都采用Builder模式,如果方法中含有多个参数,尤其是当它们中有些是可选的时候,最好定义一个对象来表示所有参数,并允许客户端在这个对象上进行多次setter调用。一旦设置了需要的参数,客户端就调用对象的”执行“方法,它对参数进行有效性检查,并执行实际的计算。
- 对于参数类型,优先使用接口而不是类。如果使用的类而不是接口,就限制了客户端只能传入特定的实现。
- 对于boolean参数,要优先使用两个元素的枚举类型。它是代码更容易阅读和编写。
第四十一条、慎用重载
对于重载(overloaded method)方法的选择是静态的,而对于被覆盖(overridden method)的方法选择则是动态的。选择被覆盖的方法的正确版本是在运行时进行的,选择的依据是被调用方法所在对象的运行时类型。
-
因为覆盖机制是规范,而重载机制是例外,所以应该避免胡乱地使用重载机制:安全而保守的策略是永远不要导出两个具有相同参数数目的重载方法。只要当两个重载方法在同样的参数上被调用时,它们执行相同的功能,重载就不会带来危害。
public boolean contentEquals(StringBuffer sb){ return contentEquals((CharSequence) sb); }
第四十二条、慎用可变参数Varargs
可变参数方法接受0或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。
-
当需要让方法带有不定数量的参数时,可变参数非常有效。
static int sum(int... args){ int sum = 0; for(int arg:args){ sum += arg; } return sum; }
不宜过度使用,使用不当会产生混乱的结果。
第四十三条、返回零长度的数据或者集合,而不是null
对于一个返回null而不是零长度数组或者集合的方法,几乎每次处理该方法都需要曲折的处理方法,很容易出错。
-
零长度数组是不可变的,可以被自由共享。在下面的习惯用法中,零长度数组常量被传递给toArray方法,以指明所期盼的返回类型。
private final List<Cheese> cheesesInStock = ...; private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0]; public Cheese[] getCheeses(){ return cheessInStock.toArray(EMPTY_CHEESE_ARRAY); }
集合值的方法也可以做成在每当需要返回空集合时都返回同一个不可变的空集合.
Collections.emptySet
,emptyList
和emptyMap
方法正是所需要的。总结:返回类型为数据或者是集合的方法没理由返回null而不是返回一个零长度的数组或者集合。
第四十四条、为所有导出的API元素编写文档注释
- Java环境中提供成为Javadoc的实用工具,利用特殊格式的文档注释doc comment,根据源代码自动生成API文档。
- 为了正确地编写API文档,必须在每个被导出类、接口、构造器、方法和域声明之前增加一个文档注释。
- 方法的文档注释应该简洁地描述它和客户端之间的约定。应该说明这个方法做了什么,而不是如何完成的。还需要列举出这个方法的所有前提条件和后置条件;还需要描述它的副作用(side effect):系统状态中可以观察到的变化。
@param
和@return
以及抛出的异常(@throws
) - 每个文档注释的第一句话成了该注释所属元素的概要描述。
- 总结:对于所有可导出的API元素来说,使用文档注释应该被看作是强制性的。