为什么要重写equals和hashCode方法?

首先我们来看看,Object类的equals方法是怎样写的:

    public boolean equals(Object obj) {
        return (this == obj);
    }

咦,不对啊,不是说好了==是比较地址,equals()是比较值么?这句话其实没说全,完整版是:使用equals()比较八大包装类对象和String类对象时,比较的是值;而对于其它对象,默认比较的是引用地址。 为什么String类对象才是比较值呢?因为人家鸡贼啊,人家默默地重写了equals方法!

  • String类重写的equals方法
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        //地址不一致再看是否此类对象
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            //对比值
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
  • equals方法重写模版(先判地址再判空再判类型)
	@Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
         
        People p = (People)obj;
        return this.name.equals(p.name)
            && this.age == p.age;
    }
  • 简洁版
	public boolean equals(Object o) {
      if (o == null || !(o instanceof Key))
      { return false; }
      else
      ...
    }

嗯,这个我懂,但是为什么重写了equals方法之后,一般都要把hashCode方法也重写了呢?这就需要咱先弄懂,hashCode()是用来干什么的。
众所周知,HashMap/Hashtable的key(HashSet)是不允许重复的,那么,当向HashSet中插入对象时,如何判别是否已经存在该对象了呢?(这里说的是equals和hashCode方法均未重写的情况)
1.调用这个对象的hashCode(),得到对应的hash值,看是否已有此hash值(哈希值由一个table维护),若无,则直接存储;
2.若存在此hash值,便调用equals()进行比较,若不同则存储,相同则覆盖。

hashCode方法就是根据一定的规则将与对象相关的信息(如存储地址、字段等)映射成一个数值,这个数值称作为散列值。

总结一下,判断两个对象相等,先hashCode()后equals()。亦即当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true。
但是现在问题是,你重写了equals(),使equals方法从地址判断变成了值判断,那有没有可能有两个对象,值相等而hashCode()却不同的呢?
哈希值是由地址和字段等因子决定的,我们在这里简化一下,直接把哈希值等同于地址(有的JVM就是这样的)。那么,上面的问题就变成了:是否有两个对象,值相等而地址不一致?答案是肯定的。再转化一下,就是equals()返回true,但是hashCode()不同。这就尴尬了,说好的hashCode()通过了再equals()呢?说好的obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()呢?所以,为了保持逻辑上的一致,就需要重写hashCode()。

  • String类重写的hashCode方法
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

如何重写hashCode方法呢?一般来说,就是依赖字段(放弃地址)来生成哈希码,从而保持逻辑上的一致性。


  • HashMap类的put方法(先检验hashCode()后equals())
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • HashMap类的get方法(依赖哈希码来get,哈希码相同则判equals())
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

问题又来了,比如说我有一个People类,有name,age,sex,city字段,在重写hashCode()时,把这4个字段都作为哈希值的因子。后来我调用put(peopleMe,“我”)把对象peopleMe插进了一个HashMap。但当我长了1岁,age+1,这时哈希值显然是要变的,虽然我还是我,但是调用get(peopleMe)时,由于哈希值变了,已经找不到“我”了。所以,在重写hashCode()和equals()时,尽量不要依赖易变的字段。

一句话概括:
(未重写时)equals()比较对象的地址;hashCode()是一个由地址等因子控制的值
(重写后)equals()比较对象的值;hashCode()重写成一个由字段控制的值

猜你喜欢

转载自blog.csdn.net/sinat_33404263/article/details/105734582