這要從 Object 類開始提及,咱們知道 Object 類是 Java 的超類,每一個類都直接或者間接的繼承了 Object 類,在 Object 中提供了 8 個基本的方法,equals 方法和 hashcode 方法就是其中的兩個。java
equals 方法:Object 類中的 equals 方法用於檢測一個對象是否等於另外一個對象,在 Object 類中,這個方法將判斷兩個對象是否具備相同的引用,若是兩個對象具備相同的引用,它們必定是相等的。程序員
hashcode 方法:用來獲取散列碼,散列碼是由對象導出的一個整數值,散列碼是沒有規律的,若是 x 和 y 是兩個不一樣的對象,那麼 x.hashCode() 與 y.hashCode() 基本上不會相同數組
爲何須要重寫 equals 方法和 hashcode 方法,我想主要是基於如下兩點來考慮:微信
一、咱們已經知道了 Object 中的 equals 方法是用來判斷兩個對象的引用是否相同,可是有時候咱們並不須要判斷兩個對象的引用是否相等,咱們只須要兩個對象的某個特定狀態是否相等。好比對於兩篇文章來講,我只要判斷兩篇文章的連接是否相同,若是連接相同,那麼它們就是同一篇文章,我並不須要去比較其它屬性或者引用地址是否相同。編輯器
二、在某些業務場景下,咱們須要使用自定義類做爲哈希表的鍵,這時候咱們就須要重寫,由於若是不作特定修改的話,每一個對象產生的 hashcode 基本上不可能相同,而 hashcode 決定了該元素在哈希表中的位置,equals 決定了判斷邏輯,因此特殊狀況下就須要重寫這兩個方法,才能符合咱們的要求。ide
咱們使用一個小 Demo 來模擬一下特殊場景,讓咱們更好的理解爲何須要重寫 equals 和 hashcode 方法,咱們的場景是:咱們有不少篇文章,我須要判斷文章是否已經存在 Set 中,兩篇文章相同的條件是訪問路徑相同。函數
好了,咱們一塊兒動手寫 Demo 吧,咱們創建一個文章類來存放文章信息,文章類具體設計以下:工具
class Article{
// 文章路徑
String url;
// 文章標題
String title;
public Article(String url ,String title){
this.url = url;
this.title = title;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
複製代碼
文章類中有路徑、標題兩個屬性,在這個類中咱們並無重寫 equals 和 hashcode 方法,因此這裏會使用超類 Object 中的 equals 和 hashcode 方法,爲了防止你沒有看過 Object 類中的 equals 和 hashcode 方法,咱們先一塊兒來看一下 Object 的類中的 equals 和 hashcode 方法:學習
看完以後,接下來,咱們編寫一個測試類,測試類代碼以下:測試
public class EqualsAndHashcode {
public static void main(String[] args) {
Article article = new Article("www.baidu.com","百度一下");
Article article1 = new Article("www.baidu.com","坑B百度");
Set<Article> set = new HashSet<>();
set.add(article);
System.out.println(set.contains(article1));
}
}
複製代碼
在測試類中,咱們實例化了兩個文章對象,文章對象的 url 都是同樣的,標題不同,咱們將 article 對象存入到 Set 中,判斷 article1 對象是否存在 Set 中,按照咱們的假設,兩篇文章的 Url 相同,則兩篇文章就應該是同一篇文章,因此這裏應該給咱們返回 True,咱們運行 Main 方法。獲得結果以下:
咱們看到告終果不是你想要的 True 而是 False ,這個緣由很簡單,由於兩篇文章的訪問路徑相同就是同一篇文章,這是咱們定義的規則,咱們並無告訴咱們的程序這個規則,咱們沒有重寫 equals 和 hashcode 方法,因此係統在判斷的時候使用的是 Object 類默認的 equals 和 hashcode 方法,默認的 equals 方法判斷的是兩個對象的引用地址是否相同,這裏確定是不同的,獲得的答案就是 False 。咱們須要把相等的規則告訴咱們的程序,那咱們就把 equals 方法重寫了。在這裏咱們先使用 IDEA 工具生成的 equals 方法,把最後的邏輯返回邏輯修改一下就行了,具體的編寫規則咱們下面會介紹。最後咱們的 equals 方法以下
/** * 重寫equals方法,只要兩篇文章的url相同就是同一篇文章 * @param o * @return */
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Article article = (Article) o;
return Objects.equals(url, article.url);
}
複製代碼
再一次運行 Main 方法,你會發現仍是 False ,這是爲何呢?我已經把判斷兩個對象相等的邏輯告訴程序了,不急,咱們先來聊一聊哈希表吧,咱們知道哈希表採用的是數組+鏈表的結構,每一個數組上掛載着鏈表,鏈表的節點用來存儲對象信息,而對象落到數組的位置由 hashcode()。因此當咱們調用 HashSet 的 add(Object o) 方法時,首先會根據o.hashCode()的返回值定位到相應的數組位置,若是該數組位置上沒有結點,則將 o 放到這裏,若是已經有結點了, 則把 o 掛到鏈表末端。同理,當調用 contains(Object o) 時,Java 會經過 hashCode()的返回值定位到相應的數組位置,而後再在對應的鏈表中的結點依次調用 equals() 方法來判斷結點中的對象是不是你想要的對象。
因爲咱們只重寫了 equals 方法並無重寫 hashcode 方法,因此兩篇文章的 hashcode 值不同,這樣映射到數組的位置就不同,調用 set.contains(article1) 方法時,在哈希表中的狀況可能以下圖所示:
article 對象被映射到了數組下標爲 0 的位置,article1 對象被映射到了數組下標爲 6 的位置,因此沒有找到返回 False。既然只重寫 equals 方法不行,那麼咱們把 hashcode 方法也重寫了。
跟 equals 方法同樣,咱們也使用 idea 編輯器幫咱們生成的 hashcode 方法,只須要作稍微的改動就能夠,具體 hashcode 代碼以下:
@Override
public int hashCode() {
return Objects.hash(url);
}
複製代碼
重寫好 hashcode 方法以後,再一次運行 Main 方法,此次獲得的結果爲 True,這會就是咱們想要的結果了。重寫 equals 和 hashcode 方法以後,在哈希表中的查找以下圖所示:
首先 article1 對象也會被映射到數組下標爲 1 的位置,在數組下標爲 1 的位置存在 article 數據節點,因此會執行 article1.equals(article) 命令,由於咱們重寫了 Article 對象的 equals 方法,這個是否會判斷兩個 Article 對象的 url 屬性是否相等,若是相等就返回 True,在這裏顯然是相等的,因此這裏就返回 True,獲得咱們想要的結果。
須要本身重寫 equals 方法?好的,我這就重寫,噼裏啪啦的敲出了下面這段代碼:
public boolean equals(Article o) {
if (this == o) return true;
if (o == null || !(o instanceof Article)) return false;
return o.url.equals(url);
}
複製代碼
這樣寫對嗎?雖然裏面的邏輯看上的沒什麼問題,可是 equals 方法的參數變成了Article。 其實你這跟重寫 equals 方法沒有半毛線關係,這徹底是從新定義了一個參數類型爲 Article 的 equals 方法,並無去覆蓋 Object 類中的 equals 方法。
那該如何重寫 equals 方法呢?其實 equals 方法是有通用規定的,當你重寫 equals 方法時,你就須要重寫 equals 方法的通用約定,在 Object 中有以下規範: equals 方法實現了一個等價關係(equivalence relation)。它有如下這些屬性:
如今咱們已經知道了寫 equals 方法的通用約定,那咱們就參照重寫 equals 方法的通用約定,再一次來重寫 Article 對象的 equals() 方法。代碼以下:
// 使用 @Override 標記,這樣就能夠避免上面的錯誤
@Override
public boolean equals(Object o) {
// 一、判斷是否等於自身
if (this == o) return true;
// 二、判斷 o 對象是否爲空 或者類型是否爲 Article
if (o == null || !(o instanceof Article)) return false;
// 三、參數類型轉換
Article article = (Article) o;
// 四、判斷兩個對象的 url 是否相等
return article.url.equals(url);
}
複製代碼
這一次咱們使用了 @Override 標記,這樣就能夠避免咱們上一個重寫的錯誤,由於父類中並無參數爲 Article 的方法,因此編譯器會報錯,這對程序員來講是很是友好的。接下來咱們進行了 自反性、非空性的驗證,最後判斷兩個對象的 url 是否相等。這個 equals 方法就比上面那個要好不少,基本上沒什麼大毛病了。
在 effective-java 書中總結了一套編寫高質量 equals 方法的配方,配方以下:
咱們已經瞭解了怎麼重寫 equals 方法了,接下來就一塊兒瞭解如何重寫 hashcode 方法,咱們知道 hashcode 方法返回的是一個 int 類型的方法,那好辦呀,像下面這樣重寫就好了
@Override
public int hashCode() {
return 1;
}
複製代碼
這樣寫對嗎?對錯先無論,咱們先來看一下 hashcode 在 Object 中的規定:
照 hashcode 規定來看,這樣寫彷佛也沒什麼問題,可是你應該知道哈希表,若是這樣寫的話,對於HashMap 和 HashSet 等散列表來講,直接把它們廢掉了,在哈列表中,元素映射到數組的哪一個位置靠 hashcode 決定,而咱們的 hashcode 始終返回 1 ,這樣的話,每一個元素都會映射到相同的位置,散列表也會退化成鏈表。
結合 hashcode 的規範和散列表來看,要重寫出一個高質量的 hashcode 方法,就須要儘量保證每一個元素產生不一樣的 hashcode 值,在 JDK 中,每一個引用類型都重寫了 hashcode 函數,咱們看看 String 類中的 hashcode 是如何重寫的:
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 方法寫的仍是很是好的,我我的比較喜歡用官方的東西,我以爲他們考慮的確定比咱們多不少,因此咱們 Article 類的 hashcode 方法就能夠這樣寫
/** * 重寫 hashcode方法,根據url返回hash值 * @return */
@Override
public int hashCode() {
return url.hashCode();
}
複製代碼
咱們直接調用 String 對象的 hashcode 方法。到此咱們的 equals 方法和 hashcode 方法都重寫完了,最後以 effective-java 裏面的一段總結結尾吧。
文章不足之處,望你們多多指點,共同窗習,共同進步
打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。