38、检查参数的有效性
非公有方法应该使用断言(assertion)来检查它们的参数。如在方法里assert a!=null;
但并不是说对参数的任何限制都是好事。有效性检查工作非常昂贵,而且有的有效性检查已经隐含在计算过程中。假如方法对于它所能接受的所有参数值都能够完成合理的工作,那么对参数的限制应该是越少越好。每当编写方法和构造器的时候,应该考虑它的参数有哪些限制。应该把这些限制写到文档中,并且在这个方法体的开头处,通过显示的检查来实施这些限制。(感觉自己实施起来不一定简单···)
39、必要时进行保护性拷贝
第一种攻击:如一个类内部有Date成员变量,在构造函数执行时,将外部Date参数传进来,但我们可通过改变该Date参数引用来改变Date值。这样就可以绕过构造时对Date的检查。为了保护实例内部信息避免受到这种攻击,对于构造器的每个可变参数进行保护性拷贝必不可少。如构造函数中:this.start = new Date(outStart.getTime());这样,就算外部调用参数outStart.setYear()等函数,也无法改变实例的start了。(保护性拷贝是在检查参数的有效性之前进行的(防止在检查参数后到拷贝之前改变外部参数),并且有效性检查是针对拷贝之后的对象,而不是针对原始的对象)。
这边没有用Date的clone方法进行保护性拷贝。因为Date是非final的,不能保证clone方法一定返回类为java.util.Date的对象,有可能返回处于恶意目的而设计的不可信子类的实例。
第二种攻击:对于get方法获得Date的成员变量,我们还是可以通过该成员返回值的方法改变成员的值,因此需要使它返回可变内部域的保护性拷贝:即get方法里用return new Date(start.getTime());,这样就不怕返回值被外部改变影响内部了。
对于是否要进行保护性拷贝需要考虑,如果类信任它的调用者不会修改内部组件,则可以不用并在类文档中清楚说明。
40、谨慎设计方法签名
(1)谨慎地选择方法的名称;(2)不要过于追求提供便利的方法(说的也不是很清楚···)。(3)避免过长的参数列表(目标是4个参数,或者更少)。
(3)共有三种方法:(1)把方法分解成多个方法;每个方法只需要这些参数的一个子集;(2)创建辅助类,用来保存参数的分组。(3)从对象到方法调用都采用Builder模式。
对于参数类型,要优先使用接口而不是类(常识)。
对于boolean参数,要优先使用两个元素的枚举类型。它使代码易于阅读和编写。
41、慎用重载
public static Stringclassify(Set<?> s){
return "Set";
}
public static Stringclassify(List<?> s){
return "List";
}
public static Stringclassify(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));
}
对于上述程序,没用输出Set、List和Unknown Collection,而是3个UnknownCollection。这是因为classify方法被重载的,而要调用哪个重载方法是在编译时做出的决定。
对于重载(overloaded)方法的选择是静态的,而对于被覆盖(overridden)的方法的选择则是动态的。如用父类引用引用子类,父类引用调用方法,最后都是调用的相应子类的方法(多态)。当调用被覆盖的方法时,对象的编译时类型不会影响到哪个方法将被执行;“最为具体的”那个覆盖版本总是会得到执行。(覆盖机制是规范,重载机制是例外)
安全保守的策略是:永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,保守策略是根本不要重载它。
自动装箱也会导致莫名bug,如:
Set<Integer>set = new TreeSet<Integer>();
List<Integer> list = new ArrayList<Integer>();
for(int i =-3;i<3;i++){
set.add(i);
list.add(i);
}
for(int i =0;i<3;i++){
set.remove(i);
list.remove(i);
}
System.out.println(set+" "+list);// 输出[-3, -2, -1] [-2, 0, 2]
对于list,输出[-2,0,2]是因为list.remove(i)调用重载方法remove(int i),它从列表的指定位置去除元素。,换成list.remove((Integer)i);就对了。
结论:至少应该避免:同一组参数只需要经过类型转换就可以被传递给不同的重载方法。如果无法避免,应该保证:当传递同样的参数时,所有重载方法的行为必须一致。
42、慎用可变参数
可变参数方法接受0个或者多个指定类型的参数。写法如,
static int sum(int...args){//…与args之间空格可有可无
int sum = 0;
for(int arg : args)
sum += arg;
return sum;
}
当要编写需要一个或者多个某种类型参数的方法,则static int sum(int firstArg, int…remainingArgs)
List<String> homophones =Arrays.asList(“to”,”too”,”two”);(参数是可变参数Integer/String等... a)这样是可以的
int[] digits = {1,2,3,4};
System.out.println(Arrays.asList("to","too","two"));// [to, too, two]
System.out.println(Arrays.asList(digits));// [[I@12b6651]
System.out.println(Arrays.asList(1,2,3,4));// [1, 2, 3, 4]
将数组转变成字符串的Arrays.asList做法是过时的(数组从Object上继承toString,因此直接在数组上调用toString,输出内存地址这些无意义字符串)。Arrays类得到了补充完整的Arrays.toString方法(不是可变参数方法,参数是特定类型的数组),专门为了将任何类型的数组转变成字符串。
可变参数方法的每次调用都会导致进行一次数组分配和初始化。如果无法承受,可以重载多个方法,如一个参数、两个参数···,一直到i个参数,后面再用可变参数。
43、返回零长度的数组或者集合,而不是null
如果数组的size()为0,返回null,将数组没有元素当成特例是不合理的。这样会要求客户端必须有额外的代码来处理null返回值。忘了写专门处理null返回值就会出错。
返回类型为数组或集合的方法没理由返回null,而不是返回一个零长度的数组或者集合。如:
private static final String[] EMPTY_STRING = new String[0];
public String[] getString(){
return st.toArray(EMPTY_STRING);
}
Collection.toArray(T[])的规范保证:如果输入数组大到足够容纳这个集合,它就将返回这个输入数组。有意思的函数,toArray只有当T[]够大才返回T,否则返回本身。其他方法如:returnCollection.emptyList();
在C语言中,数组长度是与实际的数组分开返回的(关键还是这个是阻碍,不然也可以返回0长度数组或集合)。因此如果返回的数组长度为0,再分配一个数组就没有任何好处···。
string——length();数组——length;集合——size();
44、为所有导出的API元素编写文档注释
为了正确编写API文档,必须在每个被导出的类、接口、构造器、方法和域声明之前增加一个文档注释。
文档注释应该列举这个类所有前提条件和后置条件。
@throws标签之后的文字应该包含单词“if”,描述这个异常将在什么样的条件下会被抛出。
{@code 语句}用于使该代码片段在生成文档中以代码字体显示,并限制HTML标记和嵌套的javadoc标签在代码片段中进行处理(即对于<这些就可以以小于的形式展现,而不是HTML的符号)。({@literal}类似,只是没有展现成代码显示的功能,其他都一样)
this被用在实例方法的文档注释中时,它应该始终是指方法调用所在的对象。
为泛型或者方法编写文档时,确保要在文档中说明所有的类型参数。
为枚举类型编写文档时,要确保在文档中说明常量,以及类型,还有任何公有方法。
为注解类型编写文档时,要确保在文档中说明所有成员,以及类型本身。
应该在文档中对类是否线程安全进行说明,如果类是可序列化的,就应该在文档中说明它的序列化形式。