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