欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

为什么重写equals方法时也要重写hashCode方法

程序员文章站 2024-03-22 17:37:46
...

本人用的jdk版本为1.8。hashCode是Object类中的一个本地方法,这意味着所有的对象继承了它的hashCode方法。废话不多说,先看Object类中对hashCode方法的介绍:

    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * The general contract of {@code hashCode} is:
     * <ul>
     * <li>Whenever it is invoked on the same object more than once during
     *     an execution of a Java application, the {@code hashCode} method
     *     must consistently return the same integer, provided no information
     *     used in {@code equals} comparisons on the object is modified.
     *     This integer need not remain consistent from one execution of an
     *     application to another execution of the same application.
     * <li>If two objects are equal according to the {@code equals(Object)}
     *     method, then calling the {@code hashCode} method on each of
     *     the two objects must produce the same integer result.
     * <li>It is <em>not</em> required that if two objects are unequal
     *     according to the {@link java.lang.Object#equals(java.lang.Object)}
     *     method, then calling the {@code hashCode} method on each of the
     *     two objects must produce distinct integer results.  However, the
     *     programmer should be aware that producing distinct integer results
     *     for unequal objects may improve the performance of hash tables.
     * </ul>
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java&trade; programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

开头就写明了该方法的存在的一个重要原因:为了支持哈希表的一些有益特性。

事实也正是如此,hashCode方法一个最重要的作用就是对所有基于散列的集合的支持。之所以这么说,是因为在类似HashMap这类集合中,put和get等操作对数据的存取都是基于key的hashCode,多说无益,来看HashMap中put和get方法的源代码。想进一步了解HashMap的话可以看:Hashmap底层实现 jdk1.8

put方法

下面是put方法的部分源代码和hash方法。可以清楚的看到,在put(key,val)时,首先是根据(n-1)&hash(key)来判断要插入的散列表的位置,其实就是靠hash(key)来决定。而hash方法的返回值是由key.hashCode()来决定的,所以key.hashCode()决定了val要插入的位置

在源代码中我们进一步看到,在决定val要插入的位置后,如果插入的位置上已经存在元素,那么就要判断要插入的元素和插入位置上的元素是否是同一个元素,因为HashMap不允许key重复,那怎么判断呢?不是根据val来判断的,是根据key的hashCode方法和equals方法来判断的。obj1==obj2表示他们是同一个对象,所以肯定有obj1.equals(obj2)返回true,可以参考:==和equals的区别

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
        // 这里的hash是hash(key)的返回值
        /*通过(n - 1) & hash计算出要插入数据的key在table(散列表)中的散列位置,并让P
          指向table中该位置上的原Node对象。如果P为null,就直接创建一个Node放到该位置就行了*/
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            /*根据key的hash值和equals()判断p与插入数据的key是否相同*/
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;        //最后的代码会让插入数据覆盖p
            else {...}

        }

所以可想而知,如果你修改了一个对象(比如User)的equals()(改成按name来比较),那么下面的代码会往hash表中插入两条数据,因为他们插入的位置都不同。

Map map = new HashMap();
map.put(new User("张三"));
map.put(new User("张三"));

get方法

下面是HashMap的get方法的部分源代码,对于map.get(key),首先也是根据(n-1)&hash(key)来确定再tables上的位置,然后根据key的hashCode方法和equals方法来遍历冲突链表来判断的,这里就不赘述了。显然对于上述测试代码,通过map.get(new User("张三");也是获取不到值的。

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //table长度大于0且key的在table中的散列位置上的Node不为空,则执行方法
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //若first的key与参数key相同,返回first
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            ......

        }
        return null;
    }

如何重写hashCode方法

很多时候我们为了省事可能会这样写:

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

但《Effective Java》中不建议,因为它使得该类的每一个对象都有同样的散列码。根据上面的讲解,HashMap是根据key.hashCode()来决定val最终存放在table中的位置,这样会导致当存入以该对象作为key的键值对时,都会映射到同一个散列桶中,使散列表退化为链表。是的本该限行时间运行的程序变成了以平方级时间在运行。对于规模很大的散列表而言,这关系到散列表能否正常工作。

书中也提到了一种重写hashCode()的策略:


生成一个 int 类型的变量 result,并且初始化一个值,比如17

  对类中每一个重要字段,也就是影响对象的值的字段,也就是 equals 方法里有比较的字段,进行以下操作:a. 计算这个字段的值 filedHashValue = filed.hashCode(); b. 执行 result = 31 * result + filedHashValue;


hashCode通用约定

在最上面的hashCode方法的注释中介绍了几个通用约定,我们应该在平时开发中遵守这些约定,如果违反的话可能导致对应类无法结合所有基于散列的集合一起正常运作。

  • 在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode 方法都必须始终返回同一个值。

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

  • 如果两个对象根据 equals 方法比较是不相等的,那么调用者两个对象中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果。但是给不相等的对象产生不同的整数散列值,是有可能提高散列表(hash table)的性能。

相关标签: java基础