Contents

Effective Java 2 Method

第38条:检查参数的有效性

绝大多数方法和构造函数都会对参数值有限制。因此,要写明参数文档并且在方法内检查。否则,运行时会产生错误。

  1. 对于公有方法,要用Javadoc的@throws标签在文档中说明违反参数值限制会抛出的异常。这些异常通常包括IllegalArguementException、IndexOutOfBoundsException或NullPointerException。
  2. 对于非公有方法,通常使用断言(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告诉我们:

  1. 它会按顺序返回list里的所有元素,在初始化的时候建立一个T类型的数组
  2. 当数组里元素个数大于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编写文档、文档注释是最好、最有效的途径。