Effective Java读书笔记 -- 第三章:对于所有对象都通用的方法

尽管Object是一个具体类,但是设计Object类主要是为了扩展。它的所有非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定,因为它们就是被设计成要被覆盖的。


第八条:覆盖equals时请遵守通用约定

    覆盖equals方法看似简单,但是很多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的方法就是不覆盖equals方法,这种情况下,类的每个实例都只与它自身相等。如果满足以下任何一个条件,这就正是所期望的结果:

        1、类的每个实例本质上都是唯一的。例如Thread类。
        2、不关心类是否提供了“逻辑相等”的测试功能。
        3、超类已经覆盖了equals,从超类继承过来的行为对于子类也是合适的。
        4、类是私有的或是包级私有的,可以确定它的equals方法永远不会被调用。

    但是如果类具有自己特有的“逻辑相等”概念(不等同于对象相等的概念),而且超类还咩有覆盖equals以实现期望的行为,这时我们就要覆盖equals方法。这通常属于“值类”的情形。

    有一种“值类”不需要覆盖equals方法,即用实例受控确保“每个值至多只存在一个对象”(即单例类)的类、枚举类型就属于这种类。对于这种类,逻辑相等与对象相等是同一回事。

    在覆盖equals方法的时候,必须要遵守它的通用约定,内容如下:

        1、自反性(reflexive)。对象必须等于其自身。

        2、对称性(symmetric)。任何两个对象对于“它们是否相等”的问题都必须保持一致。
        3、传递性(transitive)。如果一个对象等于第二个对象,并且第二个对象又等于第三个对象,则第一个对象一定等于第三个对象。
        4、一致性(consistent)。如果两个对象相等,它们就必须时钟保持相等,除非它们中有一个对象(或者两个都)被修改了。
        5、非空性(Non-nullity)。对于任何非null的引用值x,x.equals(null)必须返回false。即所有的对象都必须不等于null。

    结合所有这些要求,得出一下实现高质量equals方法的诀窍:

        1、使用==操作符检查“参数是否为这个对象的引用”。如果是,则返回true。这算是一种性能优化。
        2、使用instanceof操作符检查“参数是否为正确的类型”。如果不是则返回false。
        3、把参数转换成正确的类型。因为转换之前进行过instanceof测试,所以确保会成功。
        4、对于该类中的每个“关键(significant)域,检查参数中的域是否域该对象中对应的域相匹配。如果这些测试全部成功,则返回true,否则返回false。
        5、当编写完成了equals方法之后,最后问自己三个问题:它是否是对称的、传递的、一致的?并编写单元测试来检验这些特性!

    下面是最后一些告诫:

        1、覆盖equals时总要覆盖hashCode(见下一条)
        2、不要企图让equals方法过于智能。过度地寻求各种等价关系,容易自找麻烦。
        3、不要将equals声明中的Object对象替换为其他的类型。(使用@Override注解可以防止这种错误)


第九条:覆盖equals时总要覆盖hashCode

    一个很常见的错误根源在于没有覆盖hashCode方法。

    覆盖hashCode方法的约定如下:

        1、在应用程序的执行期间,只要对象equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。在同一个程序的多次执行过程中,每次执行所返回的整数可以不一致。
        2、如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象hashCode方法都必须产生同样的整数结果。
        3、如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象hashCode方法,则不一定要产生不同不同的整数结果。但是我们应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hash table)的性能。 

    考虑下面的类:

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;
    public PhoneNumber(int areaCode,int prefix,int lineNumber) {
        rangeCheck(areaCode,999,"area code");
        rangeCheck(prefix,999,"prefix");
        rangeCheck(lineNumber,9999,"lineNumber");
        this.areaCode = (short)areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short)lineNumber;


    }
    private static void rangeCheck(int arg,int max,String name) {
        if(arg < 0 || arg > max) { 
            throw new IllegalArgumentException(name + ": " + arg);


            }

    }
    @Override
    public boolean equals(Object o) {
        if(o == this)
            return true;
        if(!(o instanceof PhoneNumber)) { 
            return false;
        }
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
        //未重写hashCode方法
        ...


    } 

}

    这时,若考虑:

Map<PhoneNumber,String> m = new HashMap<>();
m.put(new PhoneNumber(123,456,789),"Jenny");

    如果期望调用:

m.get(new PhoneNumber(123,456,789));

返回的是“Jenny”的话,实际上无法做到,因为它返回的是null,因为这里有两个PhoneNumber实例,第一个被插入到Map的散列桶中,第二个用于获取该对象,但两个对象的散列码不同,因为hashCode默认返回的是对象的地址值,get方法会首先判断Map中是否有与目标对象的hashCode相同的对象,显然,这是两个对象,hashCode明显不同,于是返回的结果为false,也就找不到了。所以需要重写hashCode方法,好的重写方式是为不相等的对象产生不相等的散列码,为相等的对象产生相等的散列码,即如果两个对象equals为true,那么两个对象的hashCode必相等,如果两个对象equals为false,那么两个对象的hashCode不相等。

   可以按照以下思路重写hashCode方法:

        1、把某个非零的常数值,比如说17,保存在一个名为result的into类型的变量中。
        2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
            a.为该域计算int类型的散列码c:
                1)如果该域是boolean类型,则计算(f?1:0).
                2)如果该域是byte、char、short或者int类型,则计算(int)f。
                3)如果该域是long类型,则计算(int)(f^(f>>>32))。
                4)如果该域是float类型,则计算Float.floatToIntBits(f)。
               5)如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2. a. 3) 为得到的long类型值计算散列值。
                6)如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来比较这个域,则同样为这个递归地调用hashCode。如果需要更复杂的比较,则为这个域计算一个“范式”,然后农户针对这个范式调用hashCode。如果这个域的值为null,则返回0(或者其他某个常数,但通常是0)。
                7)如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据2. b的做法把这些散列值组合起来。
            b.按照下面的工时,把步骤2. a中计算得到的散列码c合并到result中:
                result = 31 * result + c;
        3、返回result。
        4、写完了hashCode方法之后,问问自己“相等的实例是否都具有相等散列码”。要编写单元测试来验证推断。

    为上述PhoneNumber类覆盖hashCode方法:

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}
    还可将缓存起来:
private volatile int hashCode;

...

@Override
public int hashCode() {
    int result = hashCode;
    if(result == 0) {
        result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
    }
    return result;
}

    

第十条:始终要覆盖toString

    java.lang.Object提供了toString方法的一个实现,它返回是:类的名称,以及一个“@”符号,接着是散列码的无符号十六进制表示法,例如"PhoneNumber@163b91"。

    在实际应用中,toString方法应该返回对象中包含的所有值得关注的信息。

    要为toString返回值中包含的所有信息,提供一种编程式的访问途径(如getter方法)。


第十一条:谨慎地覆盖clone

    如需要克隆对象,需要实现Cloneable接口。

    有关clone方法的详解,可以参考这篇文章:

    详解Java中的clone方法 – 原型模式


第十二条:考虑实现Comparable接口

    compareTo方法并没有在Object中声明。它是Comparable接口中唯一的方法。compareTo方法不单允许进行简单的等同性比较,而且允许顺序比较。除此之外,它与Object的equals方法具有相似的特征,它还是个泛型。类实现了Comparable接口,就表明它的实例具有内在的排序关系。为实现Comparable接口的对象数组进行排序非常简单:Arrays.sort(a);。对存储在集合中的Comparable对象进行搜索、计算极限值以及自动维护也同样简单。例如,下面的代码依赖于String实现了Comparable接口,它去除命令行参数列表中的重复参数,并按字母顺序打印出来:

package Effective_Java;

import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;

/**
 * @author [email protected]
 * @date 18-5-27  下午12:11
 */

public class Demo1 {
    public static void main(String[] args) {
        Set<String> set = new TreeSet<>();
        Collections.addAll(set, args);
        System.out.println(set);
    }
}

    一旦类实现了Comparable接口,它就可以跟许多泛型算法以及依赖于该接口的集合实现进行协作。付出很小的努力就可以获得非常强大的功能。

    compareTo方法的通用约定与equals方法的相似:将这个对象与指定的对象进行比较。当该对象小雨、等于或大于指定对象的时候,分别返回一个负整数、零或者正整数。如果由于指定的对象的类型而无法与该对象进行比较,则抛出ClassCastException异常。

package Effective_Java;

/**
 * @author [email protected]
 * @date 18-5-27  下午12:11
 */

public class Demo1 implements Comparable<Demo1> {
    public int a, b, c;

    public Demo1(int a, int b, int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    /**
     * Compares this object with the specified object for order.  Returns a
     * negative integer, zero, or a positive integer as this object is less
     * than, equal to, or greater than the specified object.
     *
     * <p>The implementor must ensure <tt>sgn(x.compareTo(y)) ==
     * -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>.  (This
     * implies that <tt>x.compareTo(y)</tt> must throw an exception iff
     * <tt>y.compareTo(x)</tt> throws an exception.)
     *
     * <p>The implementor must also ensure that the relation is transitive:
     * <tt>(x.compareTo(y)>0 && y.compareTo(z)>0)</tt> implies
     * <tt>x.compareTo(z)>0</tt>.
     *
     * <p>Finally, the implementor must ensure that <tt>x.compareTo(y)==0</tt>
     * implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for
     * all <tt>z</tt>.
     *
     * <p>It is strongly recommended, but <i>not</i> strictly required that
     * <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>.  Generally speaking, any
     * class that implements the <tt>Comparable</tt> interface and violates
     * this condition should clearly indicate this fact.  The recommended
     * language is "Note: this class has a natural ordering that is
     * inconsistent with equals."
     *
     * <p>In the foregoing description, the notation
     * <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
     * <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
     * <tt>0</tt>, or <tt>1</tt> according to whether the value of
     * <i>expression</i> is negative, zero or positive.
     *
     * @param o the object to be compared.
     * @return a negative integer, zero, or a positive integer as this object
     * is less than, equal to, or greater than the specified object.
     * @throws NullPointerException if the specified object is null
     * @throws ClassCastException   if the specified object's type prevents it
     *                              from being compared to this object.
     */
    @Override
    public int compareTo(Demo1 o) {
        int result = a - o.a;
        if(result != 0) {
            return result;
        }
        result = b- o.b;
        if(result != 0) {
            return result;
        }
        return c - o.c;
    }
}

总结:本章主要讲解了在面向对象(对象在哪!)编程时,需要对Object类中的一些方法进行覆盖时所要注意的一些点,以及在涉及到比较操作时,实现Comparable接口所需要注意的一些事情。共勉!



猜你喜欢

转载自blog.csdn.net/weixin_41704428/article/details/80469374
今日推荐