一、需求
對比兩個對象是否相等。對於下面的 User 對象,只需姓名和年齡相等則認爲是同一個對象。
二、解決方案
需要重寫對象的 equals 方法和 hashCode 方法
public class User {
private String id;
private String name;
private String age;
public User(){}
public User(String id, String name, String age){
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return this.id + " " + this.name + " " + this.age;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;//地址相等
}
if(obj == null){
return false;//非空性:對於任意非空引用x,x.equals(null)應該返回false。
}
if(obj instanceof User){
User other = (User) obj;
//需要比較的字段相等,則這兩個對象相等
if(Objects.equals(this.name, other.name)
&& Objects.equals(this.age, other.age)){
return true;
}
}
return false;
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + (age == null ? 0 : age.hashCode());
return result;
}
}
三、測試
創建兩個對象,名字和年齡相等則對象 equals 爲 true。
@Test
public void testEqualsObj(){
User user1 = new User("1", "xiaohua", "14");
User user2 = new User("2", "xiaohua", "14");
System.out.println((user1.equals(user2)));//打印爲 true
}
四、爲什麼要重寫 equals 方法
因爲不重寫 equals 方法,執行 user1.equals(user2)
比較的就是兩個對象的地址(即 user1 == user2),肯定是不相等的,見 Object 源碼:
public boolean equals(Object obj) {
return (this == obj);
}
五、爲什麼要重寫 hashCode 方法
既然比較兩個對象是否相等,使用的是 equals 方法,那麼只要重寫了 equals 方法就好了,幹嘛又要重寫 hashCode 方法呢?
其實當 equals 方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。那這又是爲什麼呢?看看下面這個例子就懂了。
User 對象的 hashCode 方法如下,沒有重寫父類的 hashCode 方法
@Override
public int hashCode() {
return super.hashCode();
}
使用 hashSet
@Test
public void testHashCodeObj(){
User user1 = new User("1", "xiaohua", "14");
User user2 = new User("2", "xiaohua", "14");
Set<User> userSet = new HashSet<>();
userSet.add(user1);
userSet.add(user2);
System.out.println(user1.equals(user2));
System.out.println(user1.hashCode() == user2.hashCode());
System.out.println(userSet);
}
結果
顯然,這不是我們要的結果,我們是希望兩個對象如果相等,那麼在使用 hashSet 存儲時也能認爲這兩個對象相等。
通過看 hashSet 的 add 方法能夠得知 add 方法裏面使用了對象的 hashCode 方法來判斷,所以我們需要重寫 hashCode 方法來達到我們想要的效果。
將 hashCode 方法重寫後,執行上面結果爲
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + (age == null ? 0 : age.hashCode());
return result;
}
所以:hashCode 是用於散列數據的快速存取,如利用 HashSet/HashMap/Hashtable 類來存儲數據時,都會根據存儲對象的 hashCode 值來進行判斷是否相同的。
六、如何重寫 hashCode
生成一個 int 類型的變量 result,並且初始化一個值,比如17
對類中每一個重要字段,也就是影響對象的值的字段,也就是 equals 方法裏有比較的字段,進行以下操作:
- a. 計算這個字段的值 filedHashValue = filed.hashCode();
- b. 執行 result = 31 * result + filedHashValue;
七、爲什麼要使用 31
看一看 String hashCode 方法的源碼:
/**
* Returns a hash code for this string. The hash code for a
* {@code String} object is computed as
* <blockquote><pre>
* s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
* </pre></blockquote>
* using {@code int} arithmetic, where {@code s[i]} is the
* <i>i</i>th character of the string, {@code n} is the length of
* the string, and {@code ^} indicates exponentiation.
* (The hash value of the empty string is zero.)
*
* @return a hash code value for this object.
*/
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 方法返回是 0。並且註釋中也給了個公式,可以瞭解瞭解。
String 源碼中也使用的 31,然後網上說有這兩點原因:
-
原因一:更少的乘積結果衝突
31是質子數中一個“不大不小”的存在,如果你使用的是一個如2的較小質數,那麼得出的乘積會在一個很小的範圍,很容易造成哈希值的衝突。而如果選擇一個100以上的質數,得出的哈希值會超出int的最大範圍,這兩種都不合適。而如果對超過 50,000 個英文單詞(由兩個不同版本的 Unix 字典合併而成)進行 hash code 運算,並使用常數 31, 33, 37, 39 和 41 作爲乘子,每個常數算出的哈希值衝突數都小於7個(國外大神做的測試),那麼這幾個數就被作爲生成hashCode值得備選乘數了。
所以從 31,33,37,39 等中間選擇了 31 的原因看原因二。 -
原因二:31 可以被JVM優化
JVM裏最有效的計算方式就是進行位運算了:
左移 << : 左邊的最高位丟棄,右邊補全0(把 << 左邊的數據*2的移動次冪)。
右移 >> : 把>>左邊的數據/2的移動次冪。
無符號右移 >>> : 無論最高位是0還是1,左邊補齊0。
所以 :31 * i = (i << 5) - i(左邊 31*2=62,右邊 2*2^5-2=62)
- 兩邊相等,JVM就可以高效的進行計算啦。。。