Effective Java 读书笔记 - 第七章
这一章主要讨论方法的设计需要注意的方面。
检查参数的有效性
大部分的构造方法和普通方法都对参数由限定,在执行具体的方法逻辑之前应该对方法的参数做校验,以便尽快发现不合要求的参数,进而暴露问题。对于类的public
方法,可以使用 Javadoc 的@throws
注解来说明参数错误时会抛出的异常,对于非public
方法,通常应该使用断言(assertions)来检查参数的有效性,比如。
private static void sort(long a[], int offset, int length) {
assert a != null;
assert offset >= 0 && offset <= a.length;
assert offset >= 0 && length <= a.length - offset;
// Method body
}
需要注意的是,有些参数,方法指定过程中并没有用到,但是会保存再来供后续使用,这类参数检查尤为重要,构造方法是这类场景的特例,通常通过构造方法对成员变量进行赋值供后续使用。比如,
public List<Integer> intArrayAsList(final int[] a) {
if(a == null) {
throw new NullPointerException();
}
return new AbstractList<Integer>() {
public Integer get(int i) {
return a[i];
}
public Integer set(int i, Integer val) {
int oldVal = a[i];
a[i] = val;
return oldVal;
}
public int size() {
return a.length;
}
};
}
对于参数检查的规则也有特殊的例外,如果参数检查消耗非常大,或者不切实际,而且参数检查隐含在实际代码执行中,那么就不需要在方法逻辑执行前对参数进行检查。比如Collections.sort(List)
方法,在方法执行过程中,会进行各个对象之间的比较,如果有个元素的类型和其他元素不一样,那么就会抛出ClassCastException
,所以在sort
方法中,不需要单独对元素类型进行检查。
但是不要认为方法的任何参数限制都是好事,方法的设计应该尽可能的自然,对方法的参数限制越少,方法的通用性就越强,所以应该根据实际需要来校验参数。
必要时做保护性拷贝
Java 是一门安全的语言,这意味着,缓冲区溢出,数组越界,野指针和其他内存错误不会出现在 Java 中,但是即使是在这样类型安全的语言中,为了编写应对各种异常情况的健壮的代码,还是需要做一些额外的工作。
一般情况下,如果不借助类本身的方法,是不可以修改类的属性的,但是考虑这样一种情况,比如编写一个代表一段时间的不可变类。
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 start() {
return start;
}
public Date end() {
return end;
}
}
第一眼看上去,这个类的对象确实是不可变的,但是如果在构造完Period
对象之后,直接修改传入到构造函数的参数,那么就可以间接修改Period
对象。由于Period
对象是不可变的,所以,在其构造方法中,可以对参数进行拷贝后赋值给其成员变量(start
,end
),其构造方法变为。
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(this.start + " after" + this.end);
}
}
需要注意的是,在给start
,end
赋值时,首先对参数进行了拷贝,利用拷贝的值构造新的对象,这样在Period
之外更改参数值,不会改变Period
对象。在复制start
,end
参数时,并没有使用java.util.Date
类的clone
方法,因为clone
方法可能会返回用户自定义的java.util.Date
类的子类,而在这个子类中,肯能会包含专门出于恶意而设计的代码,比如,在静态代码块中记录一个指向该对象的引用,从而让外部可以访问该引用。
然后,在构造方法里做了复制参数值的改进后,Period
类的对象仍然存在被修改的风险。比如调用Period.end()
方法就可以获取到Period
的对象的end
成员变量,而这个变量是一个对象,所以后续可以继续对这个对象做更改。所以对start()
和end()
方法的改进如下。
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
所以对于设置到对象的成员变量的引用值以及通过方法返回的引用,都必须考虑是否会被更改,如果可能被更改,那么就必须做保护性拷贝。在为成员变量选择日期类型时,可以考虑优先使用long
替换Date
。
谨慎定义方法签名
这是若干 API 设计总结,这些技巧将有助于设计易于学习和使用的 API,并且设计出的 API 不容易出错。
- 谨慎选择方法名 方法名应该易于理解和使用,并且与同一个 package 中的方法名称风格一致。
- 不要提供过多的便利方法 如果一个类提供的方法过多,那么会导致该类难以学习,使用,测试和维护。所以对于接口支持的每个功能,提供一个完整的方法,只有当某个操作使用较多时才考虑提供一个便利方法。如果不确定,按么就不要提供。
- 避免过长的参数列表
参数列表过长,将使得方法难以使用,所以方法参数个数应当不超过 4 个。有三种技巧可以减少方法参数个数。
- 将一个参数过多的方法分割成多个方法,这种方式可能会导致方法过多,应对策略是提升方法的正交性(多个方法可以组合完成一个操作)。
- 创建一个辅助类来保存参数。
- 结合前两种技巧,采用
Builder
模式。
-
优先选择接口而不是类作为参数类型
- 优先选择两个值的枚举类型而不是布尔类型 易于扩展
谨慎使用重载
先看这样一段代码,其实写这段代码时期望它输出 SET,List,Unknow Collection 这样的三行文本,但是实际上输出的却时三行 Unknow Collection 的文本。为什么会这样呢?原因是这几个方法时在一个类中的重载方法,而调用重载方法的参数类型是在编译时就确定的,也就是说在编译后,就确定了调用的重载方法时哪个。
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}
public static String classify(List<?> lst) {
return "List";
}
public static String classify(Collection<?> c) {
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));
}
}
}
与之相反,重写(也称覆盖,Override)的方法调用时在运行时动态确定的。
如果能够明显的区分一对重载方法的参数类型,那么使用重载是安全的,否则为了在使用重载机制时不会导致混乱,一种安全且保守的策略是:不要暴露两个方法参数个数相同的重写方法,如果一个方法有可变参数,那么不要重写该方法。例如ObjectOutputStream
类中有writeBoolean(boolean)
,writeInt(int)
和readBoolean()
,readInt()
等方法,虽然方法作用类似,但是通过具体的方法名加以区分。另外,对于构造方法,没法通过方法名的方式加以区分,如果有必要,可以使用静态工厂的方法来封装构造方法。
谨慎使用可变参数
可变参数的机制是先创建一个参数个数大小的数组,然后将参数值传递到数组中,最后将数组传递给方法。但是可变参数有时却不太适合,因为可变参数列表接收 0 个或多个元素,假设有个方法接收一个或多个参数,那么在使用可变参数时,就需要加上一段检查参数个数的代码,显然这样设计方法是不优雅的,并且如果参数个数为0,也没法在编译时,就发现问题。针对这种需求可以将方法设计为第一个参数是所需要的类型参数,第二个参数是可变参数列表,如
static int fun(int arg1, int... args) {
}
这样就强制调用者至少传一个参数。
可变参数给方法设计带来了很大的便利性,但是在设计方法时,也不要过度使用,如果使用不当会产生意想不到的结果。比如在 JDK 1.4 版本中,System.out.println(Array.asList(new int[]{1,2,3}))
会打印出数组的元素的字符串,但是在 JDK 1.5 版本发布以后,就只打印数组对象的字符串(即数组的 toString 结果值。)
返回值使用空数组或集合代替null
对于一个返回数组或者集合类型的方法,如果返回了null
值,那么很可能会带来不必要的麻烦,比如在绝大多数场景下返回值都会有一个或多个元素,但是如果在调用时,没有对代码做非 null 值检查,那么很可能会导致一个潜在的 bug,所以应该使用空数组或者空集合代替null
作为返回值。空集合可以使用Collections.emptySet
,Collections.emptyList
或者Collections.emptyMap
等方法来构造,空数组可以在代码中定义常量空数组来构造,比如private static final Period[] EMPTY_ARRAY = new Period[0];
。