本文大多数内容适用于构造器,也适用于普通方法,焦点集中在可用性、健壮性和灵活性上。
1.检查参数的有效性
一个原则:应该在发生错误之后尽快检测出错误
不检查参数有效性的后果:
- 方法在处理的过程中失败,并且产生令人费解的异常
- 方法正常返回,但计算出错误的结果
- 方法正常返回,但却使得某个对象处于被破坏的状态,将来在某个不确定的时候引发错误
常见方法参数的一些限制:
- 索引值必须是非负数
- 集合类的索引不能大于集合长度-1
- 对象引用不能为 null
public 方法参数有效性检查
步骤:
- 用 Javadoc 的
@throw
标签在文档中说明违法参数值限制时抛出的异常,异常通常为 IllegalArgumentException、IndexOutOfBoundsException、NullPointerException等 - 公有方法内部进行参数有效性检查,并抛出相应异常
/**
* Returns a BigInteger whose value is.
* @param m
* @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);
}
// Do the computation
}
private/package-private 方法参数有效性检查
非公有方法通常应该使用断言来检查它们的参数有效性
断言与普通有效性检查的区别:
- 断言如果失败,抛出 AssertionError
- 如果它们没起到作用,本质上也不会有成本开销
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;
// next
}
构造方法参数有效性检查
对于有些参数,方法本身没有用到,但被保存起来供以后使用。比如静态工厂方法、构造方法。检查参数有效性非常重要,避免构造出来的对象违反了这个类的约束条件
不需要检查参数有效性的情况
- 有效性检查很昂贵,或者不切实际,比如
sort
方法,检查集合中每一个对象是否可以比较 - 计算会隐式执行必要的有效性检查,比如
sort
方法,如果对象不能比较,就会抛出 ClassCastException
总结
- 编写方法前,考虑好它的参数有哪些限制
- 把限制写到方法开头的文档中
- 通过显式的检查来实施限制
2.必要时进行保护性拷贝
先介绍一个概念,不可变性,之前介绍过,要尽可能创建不可变的类,因为它有很多优点,其中创建不可变类有几条规则:
- 如果类具有指向可变对象的域,必须确保该类的客户端无法获得执行这些对象的引用
- 在构造器中,永远不要用客户端提供的对象引用来初始化这样的域
- 在访问方法中,也不要返回该对象引用
- 在构造器、访问方法和 readObject 方法中请使用保护性拷贝技术
创建 Period 类,由于Date类是可变的,所以外部可能拿到内部的 start 和 end 信息,进而会修改这个信息
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
对于构造器的每个可变参数进行保护性拷贝
为了避免内部信息被攻击,对于构造器的每个可变参数进行保护性拷贝,创建新的对象,而不是使用客户端传入的对象
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
注意:
- 保护性拷贝是在检查参数有效性之前进行
- 保护性拷贝,没有使用
clone
方法,因为 Date 是非 final 的,clone 方法不能保证一定会返回 Date 对象,可能返回出于恶意目的而设计的不可信子类的实例
使访问方法返回可变内部域进行保护性拷贝
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
总结
- 参数(和返回值)的保护性拷贝不仅仅针对不可变类,每当允许客户提供的对象进入内部数据结构中,则有必要考虑一下,客户提供的对象是否有可能是变化的
- 长度非零的数组总是可变的,在把内部数组返回给客户端时,总要进行保护性拷贝
- 真正的启示:尽可能使用不可变对象,不必再担心保护性拷贝
- 对于Date,通常不要直接使用Date的引用,而是使用Date.getTime()返回的long基本类型作为时间的表示,防止Date对象的可变性导致的问题
最后,如果类具有从客户端得到或者返回到客户端的可变组件,类就必须保护性地拷贝这些组件,如果拷贝的成本受到限制,并且类信任它的客户端不会不恰当地修改组件,就可以在文档中指明客户端的职责是不得修改受到影响的组件,以此来代替保护性拷贝。
3.谨慎设计方法签名
API 设计技巧总结:
- 谨慎地选择方法的名称
- 易于理解
- 与同一个包中的其他名称风格一致的名称
- 选择与大众认可的名称
- 不要过于追求提供便利的方法
- 避免过长的参数列表
- 四个参数或更少
缩短长参数列表的方式:
- 把方法分解成多个方法,每个方法只需要这些参数的一个子集
- 创建辅助类,用来保存参数的分组,一般是静态成员类,如果一个频繁出现的参数序列可以被看作代表了某个独特实体,则建议使用这种方式
- 使用Builder模式,参数很多,且有些是可选的。
类参数的使用技巧:
- 对于参数类型,要优先使用接口而不是类
- 比如,没有理由使用 HashMap 作为参数,应当使用 Map 接口作为参数
- 对于 boolean 参数,要优先使用两个元素的枚举类型
- 代码更易于阅读和编写
4.慎用重载
重载方法的选择是静态的,是在编译时做出决定的,只能调用与此明确对应的重载方法,而不是其父类或者子类。与此不同的是覆盖方法,覆盖方法是动态的,是在运行时决定要调用子类还是父类的方法。
看一个例子。
private class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> s) {
return "List";
}
public static String classify(Collection<?> s) {
return "Unknown Collection";
}
public static void main(String[] args) {
Collection<?>[] collections = {
new HashSet<String>(),
new ArrayList<BigInteger>(),
new HashMap<String, String>().values()
};
for (Collection<?> c : collections) {
System.out.println(classify(c));
}
}
}
结果,打印了三次 “Unknown Collection”,对于for循环的三次迭代,只能调用在编译时确定的参数为 Collection<?> 的方法。
解决方案:用单个方法替换三个重载方法
public static String classify(Collection<?> c) {
return c instanceof Set ? "Set" :
c instanceof List ? "List" : "Unknown Collection";
}
普通方法避免重载
具体,对于write方法,如果就有变形,不应该使用重载,而是增加诸如writeBoolean、writeInte这样的签名方法。
构造器方法避免重载
不能使用不同名称的构造器,但可以选择导出静态工厂,而不是重载构造器
总结
“能够重载方法”并不意味着“应该重载方法”。一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。
5.慎用可变参数
可变参数:可变参数方法接口0个或者多个指定类型的参数。可变参数机制通过先创建一个数组,数组的大小为调用位置所传递的参数数量,然后将参数值传到数组中,最后将数组传递给方法。
可变参数性能问题:可变参数方法的每次调用功能会导致进行一次数组分配和初始化。
只有对于参数数目不确定的情况,才会使用可变参数。
6.返回零长度的数组或者集合,而不是 null
对于一个返回null而不是零长度的数组或者集合的方法,编写客户端的程序员很可能会忘记这种专门的代码来处理null返回值。
返回零长度数组不会增加开销,零长度数组是不可变的,是自由共享的。