前言:Java 對象如果要比較是否相等,則需要重寫 equals 方法,同時重寫 hashCode 方法,而且 hashCode 方法里面使用質(zhì)數(shù) 31。接下來看看各種為什么。
對比兩個對象是否相等。對于下面的 User 對象,只需姓名和年齡相等則認為是同一個對象。
需要重寫對象的 equals 方法和 hashCode 方法
package com.yule.user.entity;import org.springframework.util.StringUtils;/** * 用戶實體 * * @author yule * @date 2018/8/6 21:51 */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; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAge() { return age; } public void setAge(String age) { 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(equalsStr(this.name, other.name) && equalsStr(this.age, other.age)){ return true; } } return false; } private boolean equalsStr(String str1, String str2){ if(StringUtils.isEmpty(str1) && StringUtils.isEmpty(str2)){ return true; } if(!StringUtils.isEmpty(str1) && str1.equals(str2)){ 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; }}
1、創(chuàng)建兩個對象,名字和年齡相等則對象 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 方法,執(zhí)行 user1.equals(user2) 比較的就是兩個對象的地址(即 user1 == user2),肯定是不相等的,見 Object 源碼:
public boolean equals(Object obj) { return (this == obj); }
既然比較兩個對象是否相等,使用的是 equals 方法,那么只要重寫了 equals 方法就好了,干嘛又要重寫 hashCode 方法呢?
其實當 equals 方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規(guī)協(xié)定,該協(xié)定聲明相等對象必須具有相等的哈希碼。那這又是為什么呢?看看下面這個例子就懂了。
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 方法重寫后,執(zhí)行上面結果為
@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 是用于散列數(shù)據(jù)的快速存取,如利用 HashSet/HashMap/Hashtable 類來存儲數(shù)據(jù)時,都會根據(jù)存儲對象的 hashCode 值來進行判斷是否相同的。
生成一個 int 類型的變量 result,并且初始化一個值,比如17
對類中每一個重要字段,也就是影響對象的值的字段,也就是 equals 方法里有比較的字段,進行以下操作:a. 計算這個字段的值 filedHashValue = filed.hashCode(); b. 執(zhí)行 result = 31 * result + filedHashValue;
看一看 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,然后網(wǎng)上說有這兩點原因:
31是質(zhì)子數(shù)中一個“不大不小”的存在,如果你使用的是一個如2的較小質(zhì)數(shù),那么得出的乘積會在一個很小的范圍,很容易造成哈希值的沖突。而如果選擇一個100以上的質(zhì)數(shù),得出的哈希值會超出int的最大范圍,這兩種都不合適。而如果對超過 50,000 個英文單詞(由兩個不同版本的 Unix 字典合并而成)進行 hash code 運算,并使用常數(shù) 31, 33, 37, 39 和 41 作為乘子,每個常數(shù)算出的哈希值沖突數(shù)都小于7個(國外大神做的測試),那么這幾個數(shù)就被作為生成hashCode值得備選乘數(shù)了。
所以從 31,33,37,39 等中間選擇了 31 的原因看原因二。
JVM里最有效的計算方式就是進行位運算了:
* 左移 << : 左邊的最高位丟棄,右邊補全0(把 << 左邊的數(shù)據(jù)*2的移動次冪)。
* 右移 >> : 把>>左邊的數(shù)據(jù)/2的移動次冪。
* 無符號右移 >>> : 無論最高位是0還是1,左邊補齊0。
所以 : 31 * i = (i << 5) - i(左邊 31*2=62,右邊 2*2^5-2=62) - 兩邊相等,JVM就可以高效的進行計算啦。。。