第38条:检查参数的有效性
绝大多数方法和构造函数都会对参数值有限制。因此,要写明参数文档并且在方法内检查。否则,运行时会产生错误。
- 对于公有方法,要用Javadoc的@throws标签在文档中说明违反参数值限制会抛出的异常。这些异常通常包括IllegalArguementException、IndexOutOfBoundsException或NullPointerException。
- 对于非公有方法,通常使用断言(assertion)来检查它们的参数
如:assert 表达式(为TRUE继续执行,为FALSE抛出AssertionError)
注意:现代编译器一般默认没有打开-ea,所以必须将-ea传递给Java解释器,才能启用断言。个人不推荐使用。
对于有些方法,方法本身没有用到,却被保存起来供以后使用(如类的构造器),这类参数的有效性极其重要
总之,在编写方法/构造器的时候,应该考虑它的参数有哪些限制,把这些限制写到文档中,并在方法开头显示的来进行限制。
第39条:必要时进行保护性拷贝
目的:为了防止客户端尽可能的破坏,必须保护性地设计程序。如下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
| 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();
this.start = start;
this.end = end;
}
public Date getStart(){return start;}
public Date getEnd(){return end;}
}
|
对上面的类进行调用
1
2
3
4
5
6
7
8
| Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
System.out.println("Start: "+p.getStart().getYear()+" End: "+p.getEnd().getYear());
start.setYear(1900);
end.setYear(1900);
System.out.println("Start: "+p.getStart().getYear()+" End: "+p.getEnd().getYear());
|
发现,虽然设置了Period为final类型,但是创建过Period对象后,仍然可以改变对象p。
所以,为了防止这种攻击对于构造器的每个可变参数进行保护性拷贝,并且使用备份对象作为Period实例的组件,而不使用原始对象
1
2
3
4
5
| public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (start.compareTo(end) > 0) throw new IllegalArgumentException();
}
|
注意:保护性拷贝是在检查参数的有效性之前进行的,并且有效性检查是针对拷贝之后的对象,而不是针对原始对象。
这样做事为了,在检查参数开始到拷贝参数之间,另一线程突然修改了参数,造成了TOCTOU(Time-Of-Check/Time-Of-Use)攻击。
同时为了防止客户端获取实例内的域而进行修改。如:p.getEnd().setYear(1900); 也要修改它们的访问方法:
1
2
3
4
5
6
| public Date getStart(){
return new Date(start.getTime());
}
public Date getEnd(){
return new Date(end.getTime());
}
|
注意这里都没有使用Date的clone方法来进行defensive copy
因为Date是非final的,不能保证clone方法一定会返回一个java.util.Date对象。如果返回一个不可信的实例,攻击者可以通过它进行修改攻击。所以:
对于参数类型可以被 不可信任方 子类化的参数,请不要使用clone方法进行保护性拷贝。
启示:应该使用不可变的对象作为对象的内部组件,如:Date.getTime()方法用来获取内部时间,因为Date是不可变的。
总结:defensive copy可能带来性能损失
- 如果类的调用者确定不会修改内部组件(如类和调用者是同一个包的双方),则可以不进行defensive copy,并指明文档
- 如果类具有从客户端得到 或者 返回到客户端的 可变组件,这个类就无法让自身抵御恶意的客户端攻击,则进行defensive copy
第40条:谨慎设计方法签名
1.谨慎地选择方法的名称
方法名称:易于理解,遵循命名规范
2.不要过于追提供便利的方法
每个方法都应该尽其所能
3.避免过长的参数列表
参数列表过长,损坏API使用。可以通过
- 1、把方法分解成多个方法
- 2、创建辅助类,来保存参数的分组
- 3、采用Builder模式
第41条:慎用重载
对于重载方法(Overload)的选择是静态的,对于重写方法(Override)的选择则是动态的。
因为重载的选择是在编译时进行的,完全基于参数的编译时类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class CollectionClassifier {
public static String classify(Set<?> s){
return "Set";
}
public static String classify(List<?> list){
return "List";
}
public static String classify(Collection<?> c){
return "Collection";
}
public static void main(String[] args) {
Collection<?>[] collection = {
new HashSet<String>(),new ArrayList<String>(),new HashMap<String,String>().values()
};
for (Collection<?> c : collection)
System.out.println(classify(c));
}
}
|
结果打印了3个Collection,如上解释。classify方法被重载了,而要调用那个重载方法是在编译时做出决定的。而编译时类型都为Collection<?>。
胡乱使用重载机制,会使用户根本不知道对于一组给定的参数,到底哪个方法会被重载,就如上面示例所示。
因此,保守的做法不要导出包含相同参数数目的重载方法对于构造器,总是被重载的,许多情况下可以使用静态工厂。
总结:
- 一般情况下,对于多个具有相同参数数目的方法来说,应该尽量避免重载方法。
- 然而,对于构造器是不可能的。所以,应该保证:当传递同样的参数时,所有重载方法的行为必须一致。个人理解:可以通过泛型解决相同参数个数重载问题
第42条:慎用可变参数
可变参数方法:
可匹配不同长度的变量的方法。可以接受0个或者多个指定类型的参数。
可变参数机制:
先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传递到数组中,最后将数组传递给方法。
如下:
1
2
3
4
5
6
| public static int sum(int... args){
int sum = 0;
for (int a : args)
sum += a;
return sum;
}
|
正如所见,当需要一个方法有不定量的参数时,可变参数就非常有效。如下:Arrays.asList()就采取了可变参数
1
2
3
4
| @SafeVarargs
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
|
其中@SafeVarargs,为Java7提供的 消除泛型可变参数警告的 注解。同时,该方法的参数类型检查会丢失,在编译阶段。
1
2
3
| List<?> a = Arrays.asList("a", 1, 1.0);//通过通配符类型接收
for (Object t : a)
System.out.println(t);//print a 1 1.0
|
查看Arrays.asList()也可以看出,其return new ArrayList<>(a);为在Arrays内部定义的私有静态内部类,不支持添加和删除。如果想要对其进行操作,如下:
1
2
3
4
5
| private static <T> List<T> asList(T... a) {
List<T> t = new ArrayList<T>();
Collections.addAll(t, a);
return t;
}
|
注意:
在重视性能情况下,使用可变参数机制要小心。因为每次调用方法都会导致一次数组分配和初始化,如果无法承受这一成本,则通过函数重载机制,定义多种不同个数参数的方法。(EnumSet.of就是采取重载方法解决性能问题)
总结:
在定义参数数目不定的方法适合,可变参数方法是一种很方便的方式,但是它们不应该被过渡滥用。如果使用不当,会产生混乱的结果。
第43条:返回零长度的数组或者集合,而不是null
这样做的目的:如果返回null,则客户端必须要有额外的代码来处理null返回值。如下:
1
2
3
4
5
6
7
8
| public Cheese[] getCheeses(){
if(cheeseInStock.size() == 0) return null;
...
}
//客户端需要对null判断
if(shop.getCheeses() == null && ...){
...
}
|
对于返回null而不是零长度数组或集合的方法,每次调用都需要进行处理,这样做很容易出错。所以如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| public class Cheese {
private final Integer price;
private final String name;
public Cheese(String name, Integer price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return "Name :"+name+" Price: "+Integer.toString(price);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class CheeseStack {
private static final List<Cheese> cheeseInStack = new ArrayList<Cheese>();
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getArrayCheeses(){
return cheeseInStack.toArray(EMPTY_CHEESE_ARRAY);
}
public List<Cheese> getListCheeses(){
if (cheeseInStack.isEmpty())
return Collections.emptyList();
else
return new ArrayList<Cheese>(cheeseInStack);
}
public void setCheeses(Cheese... cheese){
Collections.addAll(cheeseInStack,cheese);
}
}
|
注意CheeseStack里返回数组和集合的方法。
其中toArray()为List接口中的 T[] toArray(T[] a),其API告诉我们:
- 它会按顺序返回list里的所有元素,在初始化的时候建立一个T类型的数组
- 当数组里元素个数大于List元素个数时,数组中第一个多出的这个元素会被代替成null添加到List里,而后面多出的元素继续会添加到List里。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
| List<String> list = new ArrayList<String>();
String[] data = {"A", "B", "C"};
Collections.addAll(list, data);
String[] a = list.toArray(new String[]{"1","2","3","4","5","6"});
for (String b : a)
System.out.println(b);
//print
//A
//B
//C
//null
//5
//6
|
所以:
- 返回空数组通过toArray方法
- 返回空集合通过Collections.emptySet();Collections.emptyList();Collections.emptyMap();
总而言之:返回类型为数组或者集合的方法,没理由返回null,而是返回一个0长度的数组或者集合。
第44条:为所以导出的API元素编写文档注释
要为API编写文档、文档注释是最好、最有效的途径。