Java基礎---爲什麼要重寫hashCode和equals方法

爲什麼要重寫hashCode和equals方法

1 複習一下Hash算法

先複習一下數據結構裏的一個知識點:在一個長度爲 n(假設是 10000)的線性表(假設是ArrayList)裏,存放着無序的數字;如果我們要找一個指定的數字,就不得不通過從頭到尾依次遍歷來查找。

我們再來觀察Hash表(這裏的Hash表純粹是數據結構上的概念,和Java無關)。它的平均查找次數接近於 1,代價相當小,關鍵是在Hash表裏,存放在其中的數據和它的存儲位置是用Hash函數關聯的。

我們假設一個Hash函數是 x*x%5。當然實際情況裏不可能用這麼簡單的Hash函數,這裏純粹爲了說明方便,而Hash表是一個長度是 11的線性表。如果我們要把 6放入其中,那麼我們首先會對 6用Hash函數計算一下,結果是 1,所以我們就把 6放入到索引號是 1這個位置。同樣如果我們要放數字 7,經過Hash函數計算, 7的結果是 4,那麼它將被放入索引是4的這個位置。這個效果如下圖所示。

img

這樣做的好處非常明顯。比如我們要從中找 6這個元素,我們可以先通過Hash函數計算 6的索引位置,然後直接從 1號索引裏找到它了。

不過我們會遇到“Hash值衝突”這個問題。比如經過Hash函數計算後, 78會有相同的Hash值,對此Java的HashMap對象採用的是**“鏈地址法”**的解決方案。效果如下圖所示

img

具體的做法是,爲所有Hash值是 i的對象建立一個同義詞鏈表。假設我們在放入 8的時候,發現 4號位置已經被佔,那麼就會新建一個鏈表結點放入 8。同樣,如果我們要找 8,那麼發現 4號索引裏不是 8,那會沿着鏈表依次查找。

雖然我們還是無法徹底避免Hash值衝突的問題,但是Hash函數設計合理,仍能保證同義詞鏈表的長度被控制在一個合理的範圍裏。這裏講的理論知識並非無的放矢,大家能在後文裏清晰地瞭解到重寫hashCode方法的重要性

2 複習Object類的hashCode和equals方法

class User{
	String username;
	String pwd;
	public User(String username, String pwd) {
		super();
		this.username = username;
		this.pwd = pwd;
	}
}
public class Test {
	public static void main(String[] args) {
		User user1=new User("admin","123");
		User user2=new User("admin","123");
		System.out.println(user1.hashCode());
		System.out.println(user2.hashCode());
		System.out.println(user1.equals(user2));
	}
}

在這裏插入圖片描述

Object類的hashCode方法返回了對象的內存地址
Object類的equals方法比較了對象的內存地址
我們看到user1和user2的內存地址不同 即是我們在創建對象的時候給了一樣的username和pwd 但因爲調用的是Object類的方法 所以返回了false

3 重寫hashCode方法和equals方法

我們先來看一下正常情況下的代碼和結果

class User{
	int id;
	String name;
	public User(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	@Override
	public int hashCode() {
		return this.id;
	}
	@Override
	public boolean equals(Object obj) {
		User u=(User)obj;
		return this.id==u.id;
	}
	
}
class Test{
	public static void main(String[] args) {
		User u1=new User(1,"小明");
		User u2=new User(1,"明明");
		HashMap<User,String> hm=new HashMap<User, String>();
		hm.put(u1,"他喜歡打籃球");
		System.out.println(hm.get(u2));
		
	}
}

在這裏插入圖片描述

在正常情況下 我們自定義了User對象 以id作爲他的唯一表示 這裏我們使用了HashMap進行存儲
我門創建了兩個對象u1和u2 但他們實際是一個人 因爲他們的id相同 小明和明明分別是一個人的大名和小名
在重寫了hashCode和equals方法之後 那麼我們將u1存進去之後 通過get方法拿出的也應該是"他喜歡打籃球"這句話 結果正常

下面我們看一下將hashCode和equals註釋的情況

class User{
	int id;
	String name;
	public User(int id, String name) {
		super();
		this.id = id;
		this.name = name;
	}
	/*
	@Override
	public int hashCode() {
		return this.id;
	}
	@Override
	public boolean equals(Object obj) {
		User u=(User)obj;
		return this.id==u.id;
	}*/
	
}
class Test{
	public static void main(String[] args) {
		User u1=new User(1,"小明");
		User u2=new User(1,"明明");
		HashMap<User,String> hm=new HashMap<User, String>();
		hm.put(u1,"他喜歡打籃球");
		System.out.println(hm.get(u2));
		
	}
}

在這裏插入圖片描述

當我們往HashMap裏放u1時,首先會調用 User這個類的 hashCode方法計算它的 hash值,隨後把 k1放入hash值所指引的內存位置。

關鍵是我們沒有在User裏定義 hashCode方法。這裏調用的仍是 Object類的 hashCode方法(所有的類都是Object的子類),而 Object類的 hashCode方法返回的 hash值其實是 u1對象的 內存地址(假設是1000)

如果我們隨後是調用 hm.get(u1),那麼我們會再次調用 hashCode方法(還是返回 u1的地址 1000),隨後根據得到的 hash值,能很快地找到 u1

但我們這裏的代碼是 hm.get(u2),當我們調用 Object類的 hashCode方法(因爲 User裏沒定義)計算 u2hash值時,其實得到的是 u2的內存地址(假設是 2000)。由於 u1u2是兩個不同的對象,所以它們的內存地址一定不會相同,也就是說它們的 hash值一定不同,這就是我們無法用 u2hash值去拿 u1的原因

當我們把User類的hashCode方法的註釋去掉後,會發現它是返回 id屬性的 hashCode值,這裏 u1u2id都是1,所以它們的 hash值是相等的

我們再來更正一下存 u1和取 u2的動作。存 u1時,是根據它 idhash值,假設這裏是 100,把 u1對象放入到對應的位置。而取 u2時,是先計算它的 hash值(由於 u2id也是 1,這個值也是 100),隨後到這個位置去找。

在這裏插入圖片描述

但結果會出乎我們意料:明明 100號位置已經有 u1 但輸出結果依然是 null。其原因就是沒有重寫 User對象的 equals方法

HashMap是用鏈地址法來處理衝突,也就是說,在 100號位置上,有可能存在着多個用鏈表形式存儲的對象。它們通過 hashCode方法返回的 hash值都是100。

img

當我們通過 u2hashCode100號位置查找時,確實會得到 u1。但 u1有可能僅僅是和 u2具有相同的 hash值,但未必和 u2相等 這個時候 就需要調用 User對象的 equals方法來判斷兩者是否相等了。

由於我們在 User對象裏沒有定義 equals方法,系統就不得不調用 Object類的 equals方法。由於 Object的固有方法是根據兩個對象的內存地址來判斷,所以 u1u2一定不會相等,這就是爲什麼依然通過 hm.get(u2)依然得到 null的原因

爲了解決這個問題,我們需要打開User類裏的equals方法的註釋 在這個方法裏 只要兩個對象都是 User類型,而且它們的 id相等,它們就相等

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