EffectiveJava--对象通用方法

本章内容:
1. 覆盖equals时请遵守通用约定
2. 覆盖equals时总要覆盖hashCode
3. 始终要覆盖toString
4. 谨慎地覆盖clone
5. 考虑实现Comparable接口

1. 覆盖equals时请遵守通用约定
    如果不覆盖equals方法,类的每个实例都只与它自身相等。以下类则不需要覆盖equals方法:
        类的每个实例本质上都是唯一的;
        不关心类是否提供了“逻辑相等”的测试功能;
        超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的;
        类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。

    如果类具有自己特有的“逻辑相等”概念(非对象等同的概念),而且超类还没有覆盖equals实现期望的行为,这时我们就需要覆盖equals方法。这通常属于“值类”的情形,一个仅仅表示值的类。
    覆盖equals方法时必须遵守的通用约定:
        自反性,对于任何非null的引用值x,x.equals(x)必须返回true。
        对称性,对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
        传递性,对于任何非null的引用值x、y和z,如果x.equals(y)返回true,且y.equals(z)也返回true,则有x.equals(z)返回true。
        一致性,对于任何非null的引用值x和y,只要比较的对象中所用的信息没有被修改,多次调用x.equals(y)返回结果必须一致。
        对于任何非null的引用值x,x.euqals(null)必须返回false。
        里氏替换原则,一个类型的任何重要属性也将适用于它的子类型,因此为该类型编写的任何方法,在它的子类型上也应该同样运行得很好。

    如何实现高质量的equals方法
(1)使用==操作符检查“参数是否为这个对象的引用”,如果是,则返回true。这只不过是一种性能优化,如果比较操作符有可能很昂贵就值得这么做。
(2)使用instanceof(判断其左边对象是否为其右边类的实例)操作符检查“参数是否为正确的类型”,如果不是,则返回false。
(3)把参数转换成正确的类型
(4)对于该类中的每个“关键”域,检查参数中的域是否与该对象中对应的域相匹配。
(5)当你编写完成了equals方法之后,应该问自己三个问题:它是否对称的、传递的、一致的?

示例如下:
@Override
public boolean equals(Object o){
    if(o == this) return true;
    if(!(o instanceof A)) return false;
    A a = (A)o;
    return a.id == id && a.name == name;
}

2. 覆盖equals时总要覆盖hashCode
    在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常动作,这样的集合包括HashMap、HashSet和Hashtable。
    JavaSE6规范约定如下:
(1)只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。
(2)如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。
(3)如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。
   
    理想情况下,散列函数应该把集合中不相等的实例均匀地分布到所有可能的散列值上。下面给出一种简单的解决方法:
(1)把某个非零的常数值,比如17,保存在一个名为result的int类型的变量中。
(2)对于对象中每个关键域f(指equals方法涉及的每个域),完成以下步骤:
    a. 为该域计算int类型的散列码c:
        boolean类型:返回(f?1:0);
        byte、char、short、int类型:返回int(f);
        long类型:返回(int)(f^(f >>> 32));
        float类型:返回Float.floatToIntBits(f);
        double类型:返回Double.doubleToLongBits(f),然后按long类型计算出int值;
        对象类型:根据上面的计算方法递归的计算此对象类型的hashCode值,或定义一个更复杂的范式来计算。对象为null时通常返回0;
        数据:把每一个域当做单独的域来处理,为每个元素计算一个散列值。
    b. 按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中
        result = 31 * result + c; // 31是一个奇素数,好处是可以用移位和减法来代替乘法,VM可以自动完成这种优化。
(3)返回result
(4)写完hashCode方法之后,一定要问问自己“相等的实例是否具有相等的散列码”。

示例如下:
@Override
public int hashCode(){
    int result = 17;
    result = 31 * result + id;
    result = 31 * result + age;
    return result;
}

    如果一个类是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列码缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果你觉得这种类型的大多数对象会被用做散列键,就应该在创建实例的时候计算散列码,否则可以选择“延迟初始化(lazily initialize)”散列码,一直到hashCode被第一次调用的时候才初始化。

3. 始终要覆盖toString
    虽然java.lang.Object提供了toString方法的一个实现,但它返回的字符串通常并不是类用用户所期望看到的。它包含类的名称,以及一个@符号,接着是散列码的无符号十六进制表示法。
    toString的通用约定指出,被返回的字符串应该是一个“简洁的,但信息丰富,并且易于阅读的表达形式”。

示例如下:
@Override
public String toString(){
    return "A[id="+id+",name="+name+"]";
}

4. 谨慎地覆盖clone
    Cloneable接口的目的是作为对象的一个mixin接口,表名这样的对象允许克隆。遗憾的是,它并没有成功地达到这个目的,主要的缺陷在于它缺少一个clone方法,Object的clone方法是受保护的,如果不借助于反射,就不能仅仅因为一个对象实现了Cloneable就可以调用clone方法。即使是反射调用也可能会失败,因为不能保证该对象一定具有可访问的clone方法。
    Cloneable并没有包含任何方法,它决定了Object中受保护的clone方法实现的行为:如果一个类实现了Cloneable,Object的clone方法就返回该对象的逐域拷贝,否则就会抛出CloneNotSupportedException异常。

    java.lang.Object规范中对Clone方法的约定如下:
        创建和返回该对象的一个拷贝。对于任何对象x,表达式 x.clone != x 将会是true,并且表达式 x.clone().getclass() == x.getClass()将会是true(父子类不满足),但这些都不是绝对的要求。虽然通常情况下,表达式 x.clone().equals(x) 将会是true,但这也不是一个绝对的要求。拷贝对象往往会导致创建它的类的一个新实例,但它同时也会要求拷贝内部的数据结构。这个过程中没有调用构造器。

    另一个实现对象拷贝的好办法是提供一个拷贝构造器或拷贝工厂。拷贝构造器只是一个构造器,它唯一的参数类型是包含该构造器的类:
        public A(A a);
    拷贝工厂是类似于拷贝构造器的静态工厂:
        public static A newInstance(A a);
    拷贝构造器及静态工厂方法都比Cloneable/clone方法具有更多的优势:它们不依赖于某一种很有风险的、语言之外的对象创建机制;它们不要求遵守尚未制定好文档的规范;它们不会与final域的正常使用发生冲突;它们不会抛出不必要的受检异常;它们不需要进行类型转换。

5. 考虑实现Comparable接口
    compareTo方法是Comparable接口唯一的方法,它不但允许进行简单的等同性比较,而且允许执行顺序比较。类实现了Comparable接口,就表明它的实例具有内在的排序关系。
    一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。你付出很小的努力就可以获得非常强大 的功能。如果你正在编写一个值类,它具有非常明显的内在排序关系,比如字母顺序、按数值顺序或者按年代顺序,那你就应该坚决考虑实现 这个接口。

    compareTo方法的通用约定如下:
        将这个对象与指定的对象进行比较,当该对象小于、等于或大于指定对象的时候 ,分别返回一个负整数、零或者正整数。如果由于指定对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。
        同时它也应该满足自反性、对象性和传递性。
        强烈建议(x.compareTo(y) ==0) == (x.equals(y)),但这并非绝对必要。一般任何实现了Comparable接口的类,若违反了这个条件,都应该明确予以说明“注意:该类具有内在的排序功能,但是与equals不一致”。

    如果一个类有多个关键域,必须从最关键的域开始,逐步进行到所有的重要域。如果某个域的比较产生了非零的结果,则整个比较操作结束,并返回该结果。如果所有的域都是相等的,则对象就是相等的,并返回零。示例如下:
public int compareTo(A a){
    if(id < a.id)    return -1;
    if(id > a.id)    return 1;
    if(age < a.age) return -1;
    if(age > a.age) return 1;
    ...
    return 0;
}
    改进如下(保证两个数的差不会溢出):
public int compareTo(A a){
    int idDiff = id - a.id;
    if(idDiff != 0)    return idDiff;
    int ageDiff = age - a.age;
    if(ageDiff != 0)    return ageDiff;
    ...
    return 0;
}

你可能感兴趣的:(EffectiveJava--对象通用方法)