Java--equals()、hashCode()作用、hash算法分析

本文主要闡述個人所學知識觀點,技能水平有限,如有不足之處,望各位大哥給小弟點建議,蟹蟹啦啦啦,文章走起咯~

一、equals()、hashCode()使用說明

1、hashCode()、equals()方法都是Object類中定義的方法即所有引用數據類型均可調用該方法;
2、Object類中的hashCode()方法,默認實現是返回對象的內部地址轉成的整數值,子類可重寫該方法,並儘可能根據自身的屬性制定規則來計算返回的hashCode值
3、Object類中的equals()方法,默認實現是返回對象的內部地址比較結果,子類可重寫該方法,並儘可能根據自身的屬性制定規則進行比較並返回比較結果

二、equals()、hashCode()作用

1、equals()用來判斷兩個對象是否相等,hashCode()方法用來提高散列結構集合的查找效率;
2、集合中判斷對象是否存在,常規做法是循環遍歷集合,一一對對象進行equals比較,比較結果爲true表示集合已存在相等對象,fasle則表示不存在相等對象。常規做法效率比較低,因此引入了hash算法來提高效率。

(圖)散列結構集合中判斷是否存在相等對象A


result1:做存儲操作時,直接添加對象A;
result2:做存儲操作時,HashMap會替換已存在相等的對象爲相同key的對象A,HashSet則直接捨棄對象A。

三、hash算法

hash算法是什麼?

HashMap的數據結構是數組table[index])+ 鏈表(Entry<K,V>或紅黑樹,如下圖所示

HashMap的底層源碼使用到了hash算法,hash算法實現過程,首先根據每個對象hashCode值,再根據hashCode值計算到自己的哈希碼(hash值),最後再根據hash值(位運算或者取模)計算對象被分配在哈希表中的table[index]即數組位置。

hash算法作用

HashMap、HashSet、HashTable等散列存儲結構集合中,判斷對象是否存在,採用hash算法,根據每個對象hashCode值,計算自己的哈希碼(hash值),再根據hash值計算對象在哈希表中的table[index]即數組位置,只需要在該table[index]的鏈表中查找,效率大大提升。


HashMap的hash算法底層源碼(JDK1.6)

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;
    }

static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

HashMap的hash算法底層源碼(JDK1.8)

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

下面通過一個例子介紹hash算法是如何通過對象的hashCode方法,計算對象會被分配到哈希表中哪個table[index]數組位置?
測試代碼

public class CalculateHashIndexTest {
    public static void main(String[] args) {
        HashMap<String,Object> hashMap = new HashMap<String,Object>(16); //初始化容量initialCapacity=16,table.length=16
        Student s1 = new Student("尹一",21);
        Student s2 = new Student("小二",22);
        Student s3 = new Student("張三",23);
        Student s4 = new Student("李四",24);
        Student s5 = new Student("王五",25);
        Student s6 = new Student("趙六",26);
        Student s7 = new Student("陳七",27);
        Student s8 = new Student("王八",28);
        Student s9 = new Student("陳九",29);
        
        hashMap.put("s1",s1);
        hashMap.put("s2",s2);
        hashMap.put("s3",s3);
        hashMap.put("s4",s4);
        hashMap.put("s5",s5);
        hashMap.put("s6",s6);
        hashMap.put("s7",s7);
        hashMap.put("s8",s8);
        hashMap.put("s9",s9);
        
        System.out.println("hashCode值====");
        System.out.println("hashCode1=" + s1.hashCode());
        System.out.println("hashCode2=" + s2.hashCode());
        System.out.println("hashCode3=" + s3.hashCode());
        System.out.println("hashCode4=" + s4.hashCode());
        System.out.println("hashCode5=" + s5.hashCode());
        System.out.println("hashCode6=" + s6.hashCode());
        System.out.println("hashCode7=" + s7.hashCode());
        System.out.println("hashCode8=" + s8.hashCode());
        System.out.println("hashCode9=" + s9.hashCode());
        
    System.out.println("=================================JDK1.8======================================");
        System.out.println("====hash值====");
        //int hash = hashCode ^ (hashCode >>> 16) JDK1.8 hash值算法
        System.out.println("hash1=" + ((s1.hashCode()) ^ (s1.hashCode() >>> 16)));
        System.out.println("hash2=" + ((s2.hashCode()) ^ (s2.hashCode() >>> 16)));
        System.out.println("hash3=" + ((s3.hashCode()) ^ (s3.hashCode() >>> 16)));
        System.out.println("hash4=" + ((s4.hashCode()) ^ (s4.hashCode() >>> 16)));
        System.out.println("hash5=" + ((s5.hashCode()) ^ (s5.hashCode() >>> 16)));
        System.out.println("hash6=" + ((s6.hashCode()) ^ (s6.hashCode() >>> 16)));
        System.out.println("hash7=" + ((s7.hashCode()) ^ (s7.hashCode() >>> 16)));
        System.out.println("hash8=" + ((s8.hashCode()) ^ (s8.hashCode() >>> 16)));
        System.out.println("hash9=" + ((s9.hashCode()) ^ (s9.hashCode() >>> 16)));
        
        System.out.println("====hash表table[index]的位置====");
        //table[index]
        //tab[i = (n - 1) & hash] JDK1.8源碼中計算對象存放在哈希表中table[index]的位置
        //index=(n - 1) & hash,n表示初始化容量initialCapacity,例子中initialCapacity=16
        System.out.println(s1.getName()+ "在hash表table[" + ((15-1) & ((s1.hashCode()) ^ (s1.hashCode() >>> 16))) + "]位置");
        System.out.println(s2.getName()+ "在hash表table[" + ((15-1) & ((s2.hashCode()) ^ (s2.hashCode() >>> 16))) + "]位置");
        System.out.println(s3.getName()+ "在hash表table[" + ((15-1) & ((s3.hashCode()) ^ (s3.hashCode() >>> 16))) + "]位置");
        System.out.println(s4.getName()+ "在hash表table[" + ((15-1) & ((s4.hashCode()) ^ (s4.hashCode() >>> 16))) + "]位置");
        System.out.println(s5.getName()+ "在hash表table[" + ((15-1) & ((s5.hashCode()) ^ (s5.hashCode() >>> 16))) + "]位置");
        System.out.println(s6.getName()+ "在hash表table[" + ((15-1) & ((s6.hashCode()) ^ (s6.hashCode() >>> 16))) + "]位置");
        System.out.println(s7.getName()+ "在hash表table[" + ((15-1) & ((s7.hashCode()) ^ (s7.hashCode() >>> 16))) + "]位置");
        System.out.println(s8.getName()+ "在hash表table[" + ((15-1) & ((s8.hashCode()) ^ (s8.hashCode() >>> 16))) + "]位置");
        System.out.println(s9.getName()+ "在hash表table[" + ((15-1) & ((s9.hashCode()) ^ (s9.hashCode() >>> 16))) + "]位置");
        System.out.println("=================================JDK1.6======================================");
        System.out.println("====hash值====");
        //int hash;
        //hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
        //hash = hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4); JDK1.6 hash值算法
        int hashCode1 = s1.hashCode();
        hashCode1 ^= (hashCode1 >>> 20) ^ (hashCode1 >>> 12);
        int hash1 = hashCode1 ^ (hashCode1 >>> 7) ^ (hashCode1 >>> 4);
        
        int hashCode2 = s2.hashCode();
        hashCode2 ^= (hashCode2 >>> 20) ^ (hashCode2 >>> 12);
        int hash2 = hashCode2 ^ (hashCode2 >>> 7) ^ (hashCode2 >>> 4);
        
        int hashCode3 = s3.hashCode();
        hashCode3 ^= (hashCode3 >>> 20) ^ (hashCode3 >>> 12);
        int hash3 = hashCode3 ^ (hashCode3 >>> 7) ^ (hashCode3 >>> 4);
        
        int hashCode4 = s4.hashCode();
        hashCode4 ^= (hashCode4 >>> 20) ^ (hashCode4 >>> 12);
        int hash4 = hashCode4 ^ (hashCode4 >>> 7) ^ (hashCode4 >>> 4);
        
        int hashCode5 = s5.hashCode();
        hashCode5 ^= (hashCode5 >>> 20) ^ (hashCode5 >>> 12);
        int hash5 = hashCode5 ^ (hashCode5 >>> 7) ^ (hashCode5 >>> 4);
        
        int hashCode6 = s6.hashCode();
        hashCode6 ^= (hashCode6 >>> 20) ^ (hashCode6 >>> 12);
        int hash6 = hashCode6 ^ (hashCode6 >>> 7) ^ (hashCode6 >>> 4);
        
        int hashCode7 = s7.hashCode();
        hashCode7 ^= (hashCode7 >>> 20) ^ (hashCode7 >>> 12);
        int hash7 = hashCode7 ^ (hashCode7 >>> 7) ^ (hashCode7 >>> 4);
        
        int hashCode8 = s8.hashCode();
        hashCode8 ^= (hashCode8 >>> 20) ^ (hashCode8 >>> 12);
        int hash8 = hashCode8 ^ (hashCode8 >>> 7) ^ (hashCode8 >>> 4);
        
        int hashCode9 = s9.hashCode();
        hashCode9 ^= (hashCode9 >>> 20) ^ (hashCode9 >>> 12);
        int hash9 = hashCode9 ^ (hashCode9 >>> 7) ^ (hashCode9 >>> 4);
        
        System.out.println("hash1=" + hash1);
        System.out.println("hash2=" + hash2);
        System.out.println("hash3=" + hash3);
        System.out.println("hash4=" + hash4);
        System.out.println("hash5=" + hash5);
        System.out.println("hash6=" + hash6);
        System.out.println("hash7=" + hash7);
        System.out.println("hash8=" + hash8);
        System.out.println("hash9=" + hash9);
        
        
        System.out.println("table[index]的位置====");
        //table[index]
        //index = hash & (table.length-1),table.length表示數組大小,等於初始化容量initialCapacity大小,即table.length=16
        System.out.println(s1.getName()+ "在hash表table[" + (hash1 & (16-1)) + "]位置");
        System.out.println(s2.getName()+ "在hash表table[" + (hash2 & (16-1)) + "]位置");
        System.out.println(s3.getName()+ "在hash表table[" + (hash3 & (16-1)) + "]位置");
        System.out.println(s4.getName()+ "在hash表table[" + (hash4 & (16-1)) + "]位置");
        System.out.println(s5.getName()+ "在hash表table[" + (hash5 & (16-1)) + "]位置");
        System.out.println(s6.getName()+ "在hash表table[" + (hash6 & (16-1)) + "]位置");
        System.out.println(s7.getName()+ "在hash表table[" + (hash7 & (16-1)) + "]位置");
        System.out.println(s8.getName()+ "在hash表table[" + (hash9 & (16-1)) + "]位置");
        System.out.println(s9.getName()+ "在hash表table[" + (hash9 & (16-1)) + "]位置");
    }

輸出結果>>>

hashCode值====
hashCode1=753459
hashCode2=752328
hashCode3=776563
hashCode4=843766
hashCode5=938801
hashCode6=1145215
hashCode7=1214401
hashCode8=939621
hashCode9=1214553
============JDK1.8=================
====hash值====
hash1=753464
hash2=752323
hash3=776568
hash4=843770
hash5=938815
hash6=1145198
hash7=1214419
hash8=939627
hash9=1214539
====hash表table[index]的位置====
尹一在hash表table[8]位置
小二在hash表table[2]位置
張三在hash表table[8]位置
李四在hash表table[10]位置
王五在hash表table[14]位置
趙六在hash表table[14]位置
陳七在hash表table[2]位置
王八在hash表table[10]位置
陳九在hash表table[10]位置
================JDK1.6=============
====hash值====
hash1=777859
hash2=777004
hash3=750561
hash4=789366
hash5=961102
hash6=1068319
hash7=1280907
hash8=962373
hash9=1279221
table[index]的位置====
尹一在hash表table[3]位置
小二在hash表table[12]位置
張三在hash表table[1]位置
李四在hash表table[6]位置
王五在hash表table[14]位置
趙六在hash表table[15]位置
陳七在hash表table[11]位置
王八在hash表table[5]位置
陳九在hash表table[5]位置

(圖)測試結果>>>對象在哈希表分配狀況(JDK1.8)

(圖)測試結果>>>對象在哈希表分配狀況(JDK1.6)

hash算法只在HashMap、HashSet、HashTable等散列存儲結構集合中有效,List等線性列表集合中無效。


四、方法重寫

1、hashCode()默認、equals()默認
例1

public class Student {
    private String name;
    private Integer age;
    
    public Student() {
        super();
    }
    public Student(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "name:" + name + ", age:" + age;
    }
}
public class TestHashSet {
    public static void main(String[] args) {
        HashSet<Object> hashSet = new HashSet<>();
        Student s1 = new Student("張三",23);
        Student s2 = new Student("張三",23);
        Student s3 = new Student("李四",24);
        
        hashSet.add(s1);
        hashSet.add(s2);
        hashSet.add(s3);
        
        Iterator<Object> iterator = hashSet.iterator();
        while (iterator.hasNext()) {
            Student stu = (Student) iterator.next();
            System.out.println(stu.toString());
        }
    }
}

>>>輸出結果
name:張三, age:23
name:張三, age:23
name:李四, age:24

先進行hashCode值比較,hashCode()方法默認返回對象內部地址對應的整數值,兩個不同對象的內部對象地址不同,hashCode值不同。此時jdk會認爲s1和s2是不相等的對象,因此set會添加s2成功,違反Set集合元素唯一特性。

2、hashCode()重寫、equals()默認
例2

public class Student {
    private String name;
    private Integer age;
    
    public Student() {
        super();
    }
    
    public Student(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }
    //set()...
    //get()...
    @Override
    public String toString() {
        return "name:" + name + ", age:" + age;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((age == null) ? 0 : age.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
}

>>>輸出結果
name:張三, age:23
name:張三, age:23
name:李四, age:24

hashCode()方法重寫後,返回根據其自身屬性計算得到對應的整數值,s1,s2屬性相同,所以計算得到的hasdCode值相同,再進行equals比較,equals默認比較對象內部地址,s1,s2兩個不同對象內部址不同,返回false,此時jdk會認爲s2和s1是不相等的對象,因此set會添加s2,違反set集合元素唯一性。

3、hashCode()默認、equals()重寫
例3

public class Student {
    private String name;
    private Integer age;
    
    public Student() {
        super();
    }
    
    public Student(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }
    //set()...
    //get()...

    @Override
    public String toString() {
        return "name:" + name + ", age:" + age;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age == null) {
            if (other.age != null)
                return false;
        } else if (!age.equals(other.age))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }


>>>輸出結果
name:張三, age:23
name:張三, age:23
name:李四, age:24

先進行hashCode值比較,hashCode()方法默認返回對象地址對應的整數值。s1,s2兩個不同對象內部地址不同,hashCode值不同,此時jdk會認爲s2和s1是不相等的對象,因此set.add(s2)成功,違反set集合元素唯一性。

4、hashCode()重寫、equals()重寫
例4

 

public class Student {
    private String name;
    private Integer age;
    
    public Student() {
        super();
    }
    
    public Student(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }
    //set()...
    //get()...

    @Override
    public String toString() {
        return "name:" + name + ", age:" + age;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((age == null) ? 0 : age.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age == null) {
            if (other.age != null)
                return false;
        } else if (!age.equals(other.age))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }


>>>輸出結果
name:張三, age:23
name:李四, age:24

hashCode()方法重寫後,返回根據其自身屬性計算得到對應的整數值,s1,s2屬性相同,所以計算得到的hasdCode值相同,再進行equals比較,equals()方法重寫後,比較的是對象的自身屬性是否一一相等,s1,s2兩個對象的屬性相等,因此equals比較結果爲true,此時jdk會認爲s2和s1是相等的對象,set.add(s2)時s2會被捨棄。

5、重寫hashCode()、重寫equals()方法,內存泄漏問題
例5

 

public class Student {
    private String name;
    private Integer age;
    
    public Student() {
        super();
    }
    
    public Student(String name, Integer age) {
        super();
        this.name = name;
        this.age = age;
    }
    //set()...
    //get()...

    @Override
    public String toString() {
        return "name:" + name + ", age:" + age;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((age == null) ? 0 : age.hashCode());
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age == null) {
            if (other.age != null)
                return false;
        } else if (!age.equals(other.age))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

}

public class HashSetTest {
    public static void main(String[] args) {
        HashSet<Object> hashSet = new HashSet<>();
        Student s1 = new Student("張三",23);
        Student s2 = new Student("李四",24);
        
        hashSet.add(s1);
        hashSet.add(s2);
        
        s1.setAge(46);
        hashSet.remove(s1);
        
        Iterator<Object> iterator = hashSet.iterator();
        while (iterator.hasNext()) {
            Student stu = (Student) iterator.next();
            System.out.println(stu.toString());
        }
    }
}


>>>輸出結果
name:張三, age:46
name:李四, age:24

set.add(s1)、set.add(s2),修改對象s1的屬性,再set.remove(s1),s1不會被刪除。
由於程序運行期間,修改了對象s1的屬性,對應的hashCode值也會改變,當set.remove(s1)時會先判斷set中是否存在s1,此時set中s1的hashCode還是修改屬性前的值,但是remove(s1)的s1的由於屬性被改變因此hashCode值也會改變,所以jdk認爲Set集合中不存在對象s1,因此不會刪除對象s1,但是用戶認爲對象s1已經刪除,導致對象s1長時間不能被釋放,導致內存泄露。
因此程序運行期間不允許修改計算hashCode值相關的對象屬性值,如果需要修改對象屬性值,則應先從集合中刪除該對象。

五、文章小結

1、散列存儲結構中要確保其唯一特性,必須同時重寫equals()、hashCode()方法;
2、兩個對象equals()比較結果爲true,則hashCode()返回值一定相等;
3、兩個對象hashCode()返回值相等,equals()比較結果不一定爲true。
4、hash算法中,通過hashCode能夠高效計算對象的存儲地址;
5、hash算法只在散列存儲結構中有效,在線性列表結構中無效;
6、程序運行期間,修改計算對象hashCode值相關的屬性值,容易導致內存泄漏;


學習資料

https://blog.csdn.net/lijiecao0226/article/details/24609559

https://www.cnblogs.com/keyi/p/7119825.html

https://blog.csdn.net/fenglibing/article/details/8905007


 

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