• hashcode和equals为何要同时重写


    浅谈为何要重写 hashcode()与equals()

        首先,这两个方法都来自于Object对象,根据API文档查看下原意。(1)public boolean equals(Objectobj),对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true;注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。(2)public int hashCode() 返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。

        我们知道,如果不重写equals,那么比较的将是对象的引用是否指向同一块内存地址,重写之后目的是为了比较两个对象的value值是否相等。特别指出,此时,利用equals比较八大包装对象(如int,float等)和String类(因为该类已重写了equals和hashcode方法)对象时,默认比较的是值,在比较其它对象都是比较的引用地址。那产生了一个问题,为什么jdk中希望我们在重写equals时,非常有必要重写hashcode呢?

        我的理解是hashcode是用于散列数据的快速存取,如利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的。这样如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致,如在存储散列集合时(如Set类),将会存储了两个值一样的对象,导致混淆,因此,就也需要重写hashcode。为了保证这种一致性,必须满足以下两个条件

        (1)当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true
        (2)当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false

         下面,通过一个简单的例子来验证一下。

    import java.util.*;

    class BeanA {
    private int i;

    public BeanA(int i) {
       this.i = i;
    }

    public String toString() {
       return "   " + i;
    }

    public boolean equals(Object o) {
       BeanA a = (BeanA) o;
       return (a.i == i) ? true : false;
    }

    public int hashCode() {
       return i;
    }

    }

    public class HashCodeTest {

    public static void main(String[] args) {
       HashSet<BeanA> set = new HashSet<BeanA>();
       for (int i = 0; i <= 3; i++){
        set.add(new BeanA(i));
       }
       System.out.println(set);
       set.add(new BeanA(1));
       System.out.println(set.toString());
       System.out.println(set.contains(new BeanA(0)));
       System.out.println(set.add(new BeanA(1)));
       System.out.println(set.add(new BeanA(4)));
       System.out.println(set);

    }

    }

    我们在类BeanA中重写了equals和hashcode方法,这样在存储到HashSet数据集中,将保证不会出现重复的数据;如果把这两个方法去掉后,那些重复的数据仍会存入HashSet中,这就与HashSet强调的元素唯一性相违背,大家可以把这两个方法注释掉再运行一下。

    因此,我们就可以理解在一些java类中什么情况下需要重写equals和hashcode。比如:在hibernate的实体类中,往往通过一个主键(或唯一标识符)来判断数据库的某一行,这就需要重写这两个方法。因为,Hibernate保证,仅在特定会话范围内,持久化标识(数据库的行)和Java标识是等价的。因此,一旦 我们混合了从不同会话中获取的实例,如果希望Set有明确的语义,就必 须实现equals() 和hashCode()。




    如果你的对象想散列存储的集合中或者想作为散列Map的Key时(HashSet、HashMap、Hashtable等)那么你必须重写equals()方法,这样才能保证唯一性。在重写equals()方法的同时,必须重写hashCode()方法?当然,在这种情况下,你不想重写hashCode()方法,也没有错,但是sun建议这么做,重写hashCode只是技术要求(为了提高效率)。

          当在散列集合中放入key时,将自动查看key对象的hashCode值,若此时放置的hashCode值和原来已有的hashCode值相等,则自动调用equals()方法,若此时返回的为true则表示该key为相同的key值,只会存在一份。

          Object中关于hashCode和equals方法的定义为:

    Java代码  收藏代码

    [java] view plaincopy
    1. public boolean equals(Object obj) {    
    2.     return (this == obj);    
    3. }    
    4. public native int hashCode();    


          基类的hashCode是一个native方法,访问操作系统底层,它得到的值是与这个对象在内存中的地址有关。

          Object的不同子类对于equals和hashCode方法有其自身的实现方式,如Integer和String等。
                equals相等的,hashCode必须相等 
                hashCode不等的,则 equals也必定不等。 
                hashCode相等的 equals不一定相等(但最好少出现 hashCode相等的情况)。

          HashMap的put(K, Value)方法提供了一个根据K的hashCode来计算Hash码的方法hash() 

    Java代码  收藏代码

    [java] view plaincopy
    1. transient Entry[] table;    
    2. public V put(K key, V value) {    
    3.     if (key == null)    
    4.         return putForNullKey(value);        //HashMap支持null的key    
    5.     int hash = hash(key.hashCode());        //根据key的hashCode计算Hash值    
    6.     int i = indexFor(hash, table.length);   //搜索指定Hash值在对应table中的索引    
    7.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {    //在i索引处Entry不为空,循环遍历e的下一个元素    
    8.         Object k;    
    9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {    
    10.             V oldValue = e.value;    
    11.             e.value = value;    
    12.             e.recordAccess(this);    
    13.             return oldValue;    
    14.         }    
    15.     }    
    16.     //若i索引处Entry为null,表明此处还没有Entry    
    17.     modCount++;    
    18.     addEntry(hash, key, value, i);  //将key、value添加到i索引处    
    19.     return null;    
    20. }    
    21.     
    22. static int hash(int h) {    
    23.     h ^= (h >>> 20) ^ (h >>> 12);    
    24.     return h ^ (h >>> 7) ^ (h >>> 4);    
    25. }    


         对于任意给定的对象,只有它的hashCode()返回值相同,那么程序调用hash(int h)方法所计算得到的Hash码值总是相同的。接下来会调用indexFor(int h, int length)方法来计算该对象应该保存在table数组的哪个索引处。 

    Java代码  收藏代码
    [java] view plaincopy
    1. static int indexFor(int h, int length) {    
    2.     return h & (length-1);    
    3. }    


         它总是通过h & (table.length - 1)来得到该对象的保存位置--而HashMap底层数组的长度总是2的n次方,这样就保证了得到的索引值总是位于table数组的索引之内。
         当通过key-value放入HashMap时,程序就根据key的hashCode()来觉得Entry的存储位置:若两个Entry的key的hashCode()相同那么他们的存储位置相同;若两个Entry的key的equals()方法返回true则新添加Entry的value将覆盖原有Entry的value,但key不会覆盖;若两个Entry的key的equals()方法返回false则新加的Entry与集合中原有的Entry形成Entry链。

    Java代码  收藏代码
    [java] view plaincopy
    1. void addEntry(int hash, K key, V value, int bucketIndex) {    
    2.     Entry<K,V> e = table[bucketIndex];    
    3.     table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    
    4.     if (size++ >= threshold)     //size保存了HashMap中所包含的key-value对的数量    
    5.         resize(2 * table.length);   //扩充table对象的长度2倍    
    6. }    


         HashSet的add(E)的实现是通过HashMap的put方法来实现的。(HashSet内部是通过HashMap来实现的,TreeSet则是通过TreeMap来实现的)

    Java代码  收藏代码
    [java] view plaincopy
    1. public V get(Object key) {    
    2.     if (key == null)    
    3.         return getForNullKey();    
    4.     int hash = hash(key.hashCode());    
    5.     int i = indexFor(hash, table.length);    
    6.     for (Entry<K,V> e = table[i]; e != null; e = e.next) {    
    7.         Object k;    
    8.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))    
    9.             return e.value;    
    10.     }    
    11.     return null;    
    12. }    


        根据key的hashCode计算器Hash值,然后取得该Hash值在表table的索引,取得该索引i对应的table中的Entry,判断key的equals()。



  • 相关阅读:
    解决问题redis问题:ERR Client sent AUTH, but no password is set
    关于使用Git的几点小技巧
    解决linux中使用git,ssh每次都要输入密码
    No package tomcatX available. 解决办法
    关于spring定时任务被多次调用的问题
    vue 非常规跨域实现 proxyTable 设置及依赖
    css 3 获取设备宽度的方法
    ionic 文本域文字输入监听事件
    Ionic3学习基础之Input组件
    微信小程序tabBar使用
  • 原文地址:https://www.cnblogs.com/daichangya/p/12958888.html
Copyright © 2020-2023  润新知