Effective Java之改写equals时总要改写hashCode

改写equals时总要改写hashCode

hashCode,就是哈希值,可以理解为一个对象的标识(好的hash,能确保不同的对象有不同的hash值),Object含有hashCode方法,用来返回对象的hash值。hashCode方法多用在基于散列值的集合类,比如HashMap、HashSet和Hashtable。

下面是hashCode的约束规范,

在一个应用程序执行期间,如果一个对象的equals方法做比较所用到的信息没有被修改的话,那么,对该对象调用hashCode方法多次,它必须返回同一个整数。在同一个应用多次执行过程中,这个整数可以不同。

如果两个对象根据equlas方法是相等的,那么调用这两个对象的hashCode方法必须产生同样的整数结果。

如果两个对象根据equals方法是不相等的。那么调用这两个对象的hashCode方法,不要求必须产生不同的整数结果。

如果你重写了类的equals方法,那么必须也重写hashCode方法。否则,就违反了上述的规范。这是因为,两个在逻辑上相等的对象(调用equals相等),必须拥有相同的hashCode,但是根据Object的hashCode,它们仅仅是两个对象,没有共同的地方。所以违背了规范2. 此时,我们就要重写hashCode方法。
实例代码1.

public class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point))
            return false;
        Point p = (Point) obj;
        return p.x == x && p.y == y;
    }

    public static void main(String[] args) {
        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        System.out.println("p1 equals p2? " + p1.equals(p2));
        System.out.println(p1.hashCode());
        System.err.println(p2.hashCode());
    }
}

在上面的例子上,我们重写了equals方法,而且能成功判断p1和p2是相等的,但是没有重写hashCode方法,所以我们调用p1.hashCode和p2.hashCode返回的值是不一样的。

那么,hashCode方法应该是怎么样的呢?编写一个合法的hashCode并不难,比如,

    public int hashCode() {
        return 41;
    }

由于hashCode规范,并没有要求不同的对象必须有不同的hashCode,所以我们可以给每个对象都返回一个相同的值。虽然这样,并没有违背hashCode的规范。但是在一些散列值存储中(HashSet、HashMap以及HashTable),却带来了灾难。

或许,你并不太了解散列值存储,我们以HashMap为例,HashMap提供了键值对(key-value)的存储,使用范例如下,

        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        HashMap<Point, String> hm = new HashMap<Point, String>();
        hm.put(p1, "p1");
        hm.put(p2, "p2");

        System.out.println(hm.get(p1));
        System.out.println(hm.get(p2));

那么,hashCode对于HashMap的作用是什么呢?
我们知道,在HashMap中,不允许两个存在两个相同的对象,那么如何判断两个对象是否相等呢?你或许会说,肯定是调用equals,是的,调用equals没有问题,但是,如果HashMap含有数万条数据,对每个对象都调用equals方法,效率肯定是一个问题。

此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。下面这段代码是java.util.HashMap的中put方法的具体实现:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
}

put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。

注意,HashMap在插入的时候,判断的是key的值是否相同

问题来了,如果我们没有重写hashCode方法,那么即使对于两个相同的对象,hashCode的结果也是不一样的(例子1),那么往HashMap中插入数据的时候,就会重复插入(注意,此时的Point并没有实现hashCode方法),

        Point p1 = new Point(2, 5);
        Point p2 = new Point(2, 5);
        HashMap<Point, String> hm = new HashMap<Point, String>();
        hm.put(p1, "p1");
        hm.put(p2, "p2");

        System.out.println("HashMap size: " + hm.size());
        System.out.println(hm.get(p1));
        System.out.println(hm.get(p2));

运行程序,我们可以发现,HashMap的大小是2. p1和p2都可以从表中取出。

如果一个类是非可变的,并且计算hashCode的代价比较大,那么应该考虑把hashCode缓存在对象内部,而不是每次都重新计算,如果对于该类的大多数对象都被用于散列键,那么可以在实例被创建的时候就计算hashCode。否则的话,可以选择迟缓初始化hashCode,一直到hashCode第一次使用才初始化。

对于前者,代码可以如下,

    private int hashCode;
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
        /**
         * 在此处初始化hashCode
         */
    }

    @Override
    public int hashCode() {
        return hashCode();
    }

在对象初始化的时候,就计算hashCode,然后在hashCode()方法中,直接返回hashCode。

对于后者,代码可以写成这样,

    private int hashCode = -1;
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public int hashCode() {
        if (hashCode() == -1) {
            /**
             *此处计算hashCode 
             */
        }
        return hashCode();
    }

这样就可以保证,hashCode只计算一次,防止多次调用hashCode带来的大量计算量。

好了,就写到这里吧,下次介绍toString的一些问题。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章