38、检查参数的有效性
绝大多数方法和构造器对于传递给它们的参数值都会有限制。如,对象引用不能为null,数组索引有范围限制等。应该在文档中指明所有这些限制,并在方法的开头处检查参数,以强制施加这些限制。
- 对于公有的方法,使用异常检查参数,并在Javadoc的@throws标签中说明违反参数限制时会抛出的异常。
- 对于非公有的方法,使用断言来检查参数。断言如果失败,将会抛出AssertionError。若它们没起作用,本质上不会有成本开销。
断言仅用于代码调试,不要在公有的API方法中使用断言,因为断言默认是关闭的。
例如:
/**
* @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)
}
....
}
//递归排序的帮助类
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;
....
}
39、必要时进行保护性拷贝
虽然java是一门安全的语言,它能对缓冲区溢出、数组越界、非法指针以及它的内存错误自动免疫,但保护性的设计程序是很有必要的。
例如,设计一个类,它表示一段不可变的时间周期
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
public Date getStart() {
return start;
}
public Date getEnd() {
return end;
}
}
这个类似乎是不可变的,并且强加了约束:起始时间(start)必须小于等于结束时间(end)。然而,由于Date类本身是可变的,并且构造函数中传递的是对象的引用,因此有可能违反这个约束条件。如:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setTime(1000);
assert p.getStart().compareTo(p.getEnd()) <= 0; //报错
因此对于构造器的每个可变参数进行保护性拷贝是必要的,并且使用备份对象作为Period实例的组件。
public 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);
}
注意:保护性拷贝必须在检查参数的有效性之前进行,并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象。这样可以避免多线程时有效性检查的正确性。对于可被子类化的参数类型,请不要使用clone方法进行保护性拷贝,因为它可能返回专门出于恶意目的而设计的不可信子类的实例。
另外由于Period的访问方法提供了对其可变内部成员的访问能力,所以可用下面方法改变Period实例:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.getEnd().setTime(1000);
assert p.getStart().compareTo(p.getEnd()) <= 0; //报错
要解决这个问题,只需修改两个方法,使它返回可变内部域的保护性拷贝即可:
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
注意:长度非零的数组总是可变的,因此,把内部数组返回给客户端之前,必须进行保护性拷贝或给客户端返回该数组的不可变视图。方法见第13条,使类和成员的可访问性最小化。
对于Period类,通常使用long基本类型作为内部时间表示法,而不是使用Date对象引用,主要是因为Date是可变的,而long是不可变的。
import java.util.*;
public final class Period {
private final long start;
private final long end;
public Period(Date start, Date end) {
this.start = start.getTime();
this.end = end.getTime();
if(this.start > this.end)
throw new IllegalArgumentException(start + " after " + end);
}
public Date getStart() {
return new Date(start);
}
public Date getEnd() {
return new Date(end);
}
public static void main(String[] args) {
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
//end.setTime(1000);
p.getEnd().setTime(1000);
assert p.getStart().compareTo(p.getEnd()) <= 0;
}
}
总之,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性的拷贝这些组件。若拷贝的成本较高,并且类信任它的客户端不会修改组件,可以在文档中指明客户端不得修改这个组件,以此来代替保护性拷贝。
40、谨慎设计方法签名
- 谨慎的选择方法的名称。方法的名称应该始终遵循标准的命名习惯。
- 不要过于追求提供便利的方法。方法太多会使类难以学习、使用、文档化、测试和维护。
- 避免过长的参数列表。4个或者更少,杜绝相同类型的长参数列表。
缩减参数列表的方法:
- 把方法分解为多个方法。
- 创建辅助类,用来保存参数的分组。
- 从对象构建到方法调用都采用Builder模式。特别是对于有些参数是可选的情况。
对于参数类型,要优先使用接口而不是类。
对于boolean参数,要优先使用两个元素的枚举类型,以便将来扩展。
41、慎用重载
对于重载方法的选择是静态的,而对于被覆盖的方法的选择则是动态的,即重载方法的选择在编译时决定,而覆盖方法的选择在运行时决定(多态,选择依据为被调用方法所在对象的运行时类型)。
例如:重载
import java.util.*;
public class OverloadTest {
public static String mothod(Collection<?> col) {
return "unknown collection";
}
public static String mothod(List<?> list) {
return "list";
}
public static void main(String[] args) {
Collection<?>[] coll = {
new HashSet<String>(),
new ArrayList<String>()
};
for(Collection<?> c : coll) {
System.out.println(mothod(c)); //都调用mothod(Collection<?> col)方法
}
}
}
程序将打印两次"unknown collection",而没有打印"list"。因为程序调用哪个重载方法是在编译时确定的,在for循环中参数的编译时类型为Collection<?>
,所以每次迭代都将调用mothod(Collection<?> col)
方法。若期望编译器根据参数的运行时类型自动将调用分发给适当的重载方法(像方法覆盖一样),将导致错误。可以使用下面的方法实现这个功能:
public static String mothod(Collection<?> col) {
return col instanceof List ? "list" : unknown collection";
}
对于方法覆盖:
public class OverrideTest {
public static void main(String[] args) {
Super[] supers = {
new Super(),
new SubClass()
};
for(Super s : supers) {
System.out.println(s.name());
}
}
}
class Super {
String name() { return "super"; }
}
class SubClass extends Super{
@Override
String name() { return "subClass";}
}
由于覆盖方法的调用时在运行时确定的,所以打印结果为"super subClass"。
注意:对于重载,jdk1.5后自动装箱可能会引起无意识的错误。例如:
List<Integer> list = new ArrayList<Integer>();
list.add(2); //调用add(E)
list.remove(2); //调用的是remove(int),不是remove(E),应该使用list.remove((Integer)2);
对于多个具有相同参数数目的方法,应尽量避免重载方法。同一组参数只需经过类型转换就可以被传递给不同的重载方法,这种情况应该被避免。若不能避免这种情况,就应该保证当传递同样的参数时,所有重载方法的行为必须一致(让具体的重载方法把调用转发给更一般的重载方法执行)。
42、慎用可变参数
可变参数机制先创建一个数组,然后将参数值传到数组中,最后将数组传递给方法。
有时要编写需要1个或多个(不是0个或多个)某种类型参数的方法,如,多个int参数的最小值
static int min(int ...args) {
if(args.length == 0) {
throw new IllegalArgumentException("Too few arguments");
}
int min = args[0];
for(int i=1; i< args.length; i++){
if(args[i] < min)
min = args[i];
}
}
这种方法的问题是,若调用这个方法时,没有传递参数,它就会在运行时而不是编译时失败,而且必须在方法内部进行有效性检查。
改进:
static int min(int firstArg, int ...remainingArgs) {
int min = firstArg;
for(int arg : remainingArgs){
if(arg < min)
min = arg;
}
}
可变参数的每次调用都会进行一次数组分配和初始化,这将影响程序性能。在定义参数数目不定的方法时,可变参数是一种很好的方式。但它们不应该被过度滥用。
43、返回零长度的数组或集合,而不是null
对于一个返回null而不是零长度数组或集合的方法,每次调用都需要额外的代码来处理null返回值。如:
private final List<String> stuNames = ....;
public String[] getNames() {
if(stuNames.size() == 0)
return null;
....
}
//调用,
String[] names = getNames();
if(names != null) {
....
}
对于上面的代码,每次使用names前都要进行判断。这样很容易出错(由于忘记进行判断)。另外认为null返回值比零长度数组更好,因为它避免了分配数组所需要的开销,这是不对的,原因有两点:
- 对于不返回任何元素的调用,因为零长度的数组是不可变的,所以可以每次都返回同一个零长度的数组。
- 返回零长度数组的调用一般为常数级的,时间复杂度为O(1),所以对程序性能影响很小。
改进:
private final List<String> stuNames = ....;
private static final Cheese[] EMPTY_STUDENT_ARRAY = new String[0];
public String[] getNames() {
return stuNames.toArray(EMPTY_STUDENT_ARRAY); //若集合为空,将使用EMPTY_STUDENT_ARRAY数组
}
Collection.toArray(T[] arg)
:如果输入数组arg大到足以容纳这个集合,它就将返回这个输入数组。
同样的对于集合的改进:
public List<String> getNames() {
if(stuNames.size() == null)
return Collection.emptyList();
else
new ArrayList<String>(stuNames); //返回一个新的对象
}
总之,返回类型为数组或集合的方法,不要返回null,而应该返回一个零长度的数组或集合。
44、为所有导出的API元素编写文档注释
Javadoc利用特殊格式的文档注释,根据源代码自动产生API文档。为了正确的编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加文档注释。若类是可序列化的,应该对它的序列化形式编写文档注释。
1、方法的文档注释应该简洁的描述出它和客户端之间的约定。除了专门为继承而设计的类外,这个约定应该说明这个方法做了什么,而不是说明它是如何完成这项工作的。文档注释应该列举出这个方法是所有前提条件和后置条件,副作用,线程安全性等。如:
/**
* Return the element at the specified position in this list
*
* <P>This method is <i>not</i> guaranteed to run in constant
* time. In some implementations it may run in time proportional
* to the element position.
*
* @param index index of element to return: must be non-negative 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
* ({@code index < 0 || index >= this.size()})
*/
E get(int index);
注:Javadoc工具会把文档注释翻译成HTML,文档中包含的HTML元素都会出现在HTML文档中。
{@code }
标签用来标记代码片段,其中的代码不会被HTML文档转译。标记多行代码使用<pre>{@code 多行代码}</pre>
。{@literal }
标签用来处理小于号(<)
、大于号(>)
、与(&)
等特殊字符,使它们能在HTML文档中显示出来。如:
//The triangle inequality is {@literal |x + y| < |x| + |y| }.
2、每个文档注释的第一句是该注释所属元素的概要描述,概要描述必须独立的描述目标元素的功能。同一个类或接口中的两个成员或方法,不应该具有同样的概要描述。(特别是重载的情况)
- 对于方法和构造器而言,概要描述应该是完整的动词性短语。如:
Collection.size()——Return the number of elements in this collection.
- 对于类、接口或域,概要描述应该是一个名称性短语。如:
TimerTask—— A task that can be scheduled for one-time or repeated execution by a Timer.
- 当为泛型类或方法编写文档时,要在文档中说明所有的类型参数。如:
/**
* An object that maps keys to values. A map cannot contain
* duplicate keys; each key can map to at most on value.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
public interface Map<K,V> {
....
}
- 为枚举类型编写文档时,要在文档中说明所有常量。
- 为注解类型编写文档时,要在文档中说明所有成员。
Javadoc具有“继承”方法注释的能力。若API元素没有文档注释,接口的文档注释优于超类的文档注释被使用。
总之,对所有可导出的API元素,都应该强制性的使用文档注释。文档编写的具体规则可以参考官方文档 How to Write Doc Comments for the Javadoc Tool。