活着是珍贵的,大多数人只是存在,仅此而已。————奥斯卡·王尔德
这是Effective Java的第六章。第五章主要讲的是如何Java来替代C语言的结构,而C语言只在编程初期入门的时候学习过,并没有真正在项目中去使用过。所以只是大致浏览了一遍,并没有详细去看。
从导读中可以知道,第六章介绍的是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 start(){
return start;
}
public Date end(){
return end;
}
}
看似是一个不可变的类,但是Date类是可变的,就容易被改变内部状态了,如
Date start = new Date();
Date end = new Date();
Period p = new Period(start,end);
end.setYear(78);
为了保护Period实例避免遭受攻击,对于构造函数的每个可变参数进行保护性拷贝(defensive copy)是有必要的。并且使用拷贝后的对象做为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);
}
}
这样修改后,前面的工具对Period就无效了。需要注意的是保护性拷贝是在检查参数有效性之前进行的,且有效性检查是针对拷贝后的对象的。同时,也要注意,不要使用Date的clone方法进行保护性拷贝。因为它是非final类的,无法保证clone方法返回的对象一定是java.util.Date类,也可能返回一个专门为了破坏而设计的子类的实例。
在上面的代码示例中,构造函数可以避免攻击了,但是两个访问方法也是Date类的,仍然可能被攻击。如
Date start = new Date();
Date end = new Date();
Period p = new Period(start,end);
p.end().setYear(78);
为了防止这种攻击,需要让它返回可变内部域的保护性拷贝即可:
public Date start(){
return (Date) start.clone();
}
public Date end(){
return (Date) end.clone();
}
到此,Period就成为了一个真正的非可变类了。
谨慎设计方法的原型
- 谨慎选择方法的名字:方法的名字要遵循标准的命名习惯,且要选择易于理解的名字。
- 不要过于追求提供便利的方法:每个方法提供其应具备的功能点即可,只有当一个操作被非常频繁的用到时,才考虑为它提供其他快捷的方法,如果不确定还是不考虑为好。避免这个类难以学习、使用、文档化、测试和维护。
- 避免长长的参数列表:参数尽量越少越好。尤其是类型相同的参数序列。因为记不住!!!
- 对于参数类型,优先使用接口而不是类;
- 谨慎使用函数对象。
谨慎地使用重载
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(),
new ArrayList(),
new HashMap().values()
};
for(int i=0;i < tests.length; i++){
System.out.println(classify(tests[i]));
}
}
}
这个程序不会打印出Set、List、Unkonw Collection,只会打印出三次Unkonw Collection。因为classify方法被重载了。对于三次迭代来说,编译时的参数类型是相同的,但是运行时是不同的。这个参数编译时的类型是Collection。
对于重载方法的选择是静态的,而对于被改写的方法的选择是动态的。对于被改写的方法,选择正确的版本是在运行时,选择的依据是被调用方法所在的对象运行时的类型。
上面程序的意图是期望编译器根据参数运行时的类型自动调用适当的重载方法。但是方法重载机制并没有提供这样的功能,如果非要实现的话,可以用一个方法来替换三个重载的方法,并做一个判断,如:
public static String classify(Collection c){
return (c instanceof Set ? "Set":
(c instanceof List ? "List" : "Unknow Collection"));
}
所以,应该尽量避免方法重载机制的混淆用法。
返回零长度的数组而不是null
当一个数组的长度为零时,如果返回的是null,就会要求客户方要用额外的代码来处理null返回值了。每次用到的时候都需要另外处理,容易造成出错。虽然不是特别重要,但是也值得注意。
为所有导出的API元素编写文档注释
- 为了正确编写API文档,需要对每个导出的类、接口、构造函数、方法和域声明前添加注释。
- 每个方法的注释应该简洁地描述出它和客户间的约定。