Effective Java读书笔记十四(Java Tips.Day.14)

来源:互联网 发布:linux 查看硬盘使用率 编辑:程序博客网 时间:2024/05/23 18:42

TIP 38 检查参数的有效性

这绝对是任何程序员都应该注意的地方。因为你编写的程序,可能会随时接收到各种千奇古怪的参数。
如果不做参数检查,可能导致以下后果:

  1. 崩掉—方法运行失败,异常退出。
  2. 不崩掉—但这是更坏的结果,因为程序看起来运行正常,但是会产生错误的结果。
  3. 不崩掉—最糟糕的情况,方法可以正常返回,但可能使某个对象状态异常,然后在后续某个不确定的时刻,爆炸,然后你根本想不到问题会出在那个该死未检查的参数。

对public方法,以及构造器方法,可以使用Javadoc的@throw标签在文档中说明违反参数限制时会抛出的异常:

    /**     * @return this mod m     * @throws new ArithmeticException if m is less than or equal to 0     */    public BigInteger mod(BigInteger m){        if (m.signum() <= 0){            throw new ArithmeticException("Modulus <= 0: "+m);        }        //do the computation        //...    }

对未被导出的方法(通常是private方法),通常应该使用断言:

    private static void sort(long a[],int offset, int length){        assert a != null;        assert offset >= 0 && offset <= a.length;        assert length >=0 && length <= a.length - offset;        //do the computation ...    }

这个方法对数组a中从offset到length-1位置的元素排序,但在此之前,要对数组a以及索引offset和数组长度length这三个参数做必要的检查。

这些断言是在声称这些条件必将为真,无论客户端会怎样调用这个方法。但不同于一般的检查,如果断言失败,将会抛出断言错误(AssertionError)。

如果这些断言没有起到作用,也不会有开销。但如果给Java解释器设置-ea或-enableassertions标记来启用它们,就会产生额外的开销。


有些特殊的情况,可以不对参数做检测。比如有时参数检查工作开销很大,或根本不切实际,而且有效性检查已隐含在计算过程中完成。
例如,考虑一个为对象列表排序的方法,Collections.sort(List),列表中的所有对象都必须是可以互相比较的。如果有对象不能互相比较,排序的某个操作就会抛出ClassCastException。这正式sort方法该做的事情,所以没有必要在排序前检查参数列表中元素是否可以相互比较。

有得必有失,如果不加选择的使用这种方法,将会导致失去失败原子性(failure atomicity)(见TIP 64)。


有时,某些计算会隐式地执行参数检查,但是如果检查不成功,就会抛出错误的异常,换句话说,由于无效的参数值而导致计算抛出的异常,与文档中标明这个方法的异常并不相符。在这种情况下,应当使用异常转译技术,将计算过程中抛出的异常转换为正确的异常。


总之,每当编写方法或者构造器的时候,就应当考虑它的参数有哪些限制,而且应当把这些限制写到文档中,并且在方法体的开头就通过显示的检查来实施这些限制。


TIP 39 必要时进行保护性拷贝

Java被认为是安全的语言,因为它对缓冲区溢出、数组越界、以及其它的内存破坏错误都自动免疫,而这些问题常常出现在诸如C/C++这样的语言中。

在一门安全语言中,在设计类的时候,程序员应当明确的知道任何导致对象状态变化的方法。例如一个类的私有field,如果不设计set方法,那么程序员就可以非常确定,在这个类之外的任何地方,field的值都不会发生变化。(反射机制暂且不理,它不属于常规编程的范畴)

然而,即使在安全语言中,程序员仍然要采取一些措施,来保证类的实例状态不会发生意外的改变。

换言之,编写一些面对客户端的不良行为时仍能保持健壮的类,是非常值得的。


看起来下面这个类是不可变的。它用于表示一个固定的时间段:

public class Period {    private final Date start;    private final Date end;    /**     *     * @param start 开始时间     * @param end   结束时间     * @throws IllegalArgumentException 如果开始时间在结束时间之后     * @throws NullPointerException 如果start或end参数为null     */    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(88); //p的内部状态被修改了

那么,怎么才能避免这种情况发生呢?
且看修改后的构造器方法:

    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实例的状态,是一定会失败的。因为p.end的实例,与传入构造器的end实例不是同一个了。所以无论怎样改变end的状态,与p.end的状态半毛钱关系都没有。

这就是保护性拷贝的作用。

请注意参数的有效性检查移到了保护性拷贝操作的后面,这是非常有必要的。在多线程并发的环境中,如果参数检查仍然放在保护性拷贝操作之前,有可能会在另一个线程中改变构造方法的参数,如果这个改变发生在危险阶段,即检查参数开始到拷贝参数之间的时间段,那么很可能导致参数检查失败,甚至更严重的、难以捕捉的错误。

同时也要注意,我们没有用到Date的clone方法来进行保护性拷贝。因为Date是非final类,不能保证clone方法一定会返回Date的对象,有可能返回一个Date子类的对象。如果这个子类将每个创建的实例加入到一个私有的静态列表中,然后允许攻击者访问这个列表,那么攻击者就可以自由地控制所有的实例。

(看到这里,豆爷才明白以往写的代码都是漏洞百出,丝毫没有考虑到安全性的问题,甚至连安全的意识都没有。)

为了阻止这种攻击,对于参数类型可以被不可信任方子类化的参数,请不要使用clone方法来进行保护性拷贝。


然而,由于start()和end()方法直接提供了对其可改变内部成员的访问能力:

    Date start = new Date();    Date end = new Date();    Period p = new Period(start,end);    p.end.setYear(88);//p的内部状态仍然被改变了...

所以,start()和end()不能直接返回start和end:

    public Date start(){        return new Date(start.getTime());    }    public Date end(){        return new Date(end.getTime());    }

这样做就对了,Period真正是不可变的了。无论程序员怎么折腾,都绝对不会违反”开始时间在结束时间之后”这个约束条件了。

不过有一点,访问方法与构造方法不同,可以使用clone来做保护性拷贝。原因很简单,在访问方法中,我们清楚地知道,Period内部的start和end的类型是Date,而不可能是其他某个不可信子类。请参考TIP 11,一般情况下,如果无法确定实例的来源类型,就使用构造器或静态工厂来做保护性拷贝。


不仅仅是针对不可变类。
每当编写方法或构造器时,如果允许客户提供的对象进入到内部数据结构中,则有必要考虑一下,客户提供的对象是否是可变的。
如果是可变的,就要考虑你的类是否能够容忍对象进入数据结构之后发生变化。如果你不允许对象进入之后还会发生变化,就必须对该对象进行保护性拷贝。
当然,如果对象是不可变的,就没有必要做这些事情了。

同理,如果要将一个内部对象返回给客户,那么对它们进行保护性拷贝也是有必要的。不管类是否为不可变的,在把一个指向内部可变组件的引用返回给客户端之前,也应该加倍认真考虑。
解决方案就是保护性拷贝。或者,如果要返回一个数组或容器,可以提供一个不可变的版本来返回。比如需要返回一个List时,可以返回一个不可变的版本:Collections.unmodifiableList(list)


总之,上述的真正启示在于,只要有可能,都应该使用不可变的对象作为对象内部的组件,这样就不必要为保护性拷贝操心。

否则,如果必须要使用可变的对象作为内部的组件,类就必须保护性拷贝这些组件,在构造方法、get和set方法中都要做这件工作。

除非,拷贝的成本/开销受到限制,并且类信任它的客户端不会瞎搞胡来,就可以在文档中指明客户端不得修改收到影响的组件,以此来代替保护性拷贝。

0 0
原创粉丝点击