標籤:hashcode equals hashmap hashset
概述
在我們使用類集架構(比如使用hashMap、hashSet)的時候,經常會涉及到重寫equals()和hashCode()這兩個方法。
這兩個方法的聯絡是:
1. 如果兩個對象不同,那麼他們的hashCode肯定不相等;
2. 如果兩個對象的hashCode相同,那麼他們也未必相等。
所以說,如果想在hashMap裡面讓兩個不相等的對象對應同一個值,首先需要讓他們的hashCode相同,其次還要讓他們的equals()方法返回true,因此為了達到這個目的,我們就只能重寫hashCode()和equals()這兩個方法了。
引用一篇文章的解說:The idea behind a Map is to be able to find an object faster than a linear search. Using hashed keys to locate objects is a two-step process. Internally the Map stores objects as an array of arrays. The index for the first array is the hashcode() value of the key. This locates the second array which is searched linearly by using equals() to determine if the object is found.
大致意思就是:使用Map比線性搜尋要快。Map儲存物件是使用數組的數組(可以理解為二維數組,這並不準確,不過可以按照這個理解。之前hashMap用的是數組,每個數組節點對應一個鏈表,現在Java 8已經把鏈表改成了treeMap,相當於一個二叉樹,這樣檢索的時候比鏈表更快,尤其是在最壞情況下,由原來鏈表的O(n)變成了二叉樹的O(logn),詳見https://dzone.com/articles/hashmap-performance),所以搜尋大致是分為兩步的,第一步是根據hashCode尋找第一維數組的下標,然後根據equals的傳回值判斷對象是第二維數組中的哪一個。
例證
舉個栗子——
import java.util.HashMap;public class Apple { private String color; public Apple(String color) { this.color = color; } public static void main(String[] args) { Apple a1 = new Apple("green"); Apple a2 = new Apple("red"); //hashMap stores apple type and its quantity HashMap<Apple, Integer> m = new HashMap<Apple, Integer>(); m.put(a1, 10); m.put(a2, 20); System.out.println(m.get(new Apple("green"))); }}
程式的運行結果為:null
此時,我們已經向hashMap裡面儲存兩個對象了,且a1就是green Apple,那麼為什麼我們通過”green”去尋找卻返回null呢?
顯然,後來我們新new出來一個對象,這和之前加入的a1綠蘋果那個對象絕對不是同一個對象,根據終極父類Object中的hashCode()的計算結果,其傳回值絕對是不一樣的。
所以——
第一步:重寫hashCode()
我們需要先讓“凡是color屬性相同的對象,其hashCode都一樣”,所以我們可以這樣重寫hashCode():
public int hashCode(){ return this.color.length();}
這裡我們使用color屬性的內容的長度作為hashCode的大小,那麼凡是green的蘋果,hashCode肯定都是5(green字串長度為5),這樣一來,屬性相同的對象的hashCode肯定都相同了。這隻是保證了一維數組的下標找到了(姑且這樣理解),還需要找第二維的下標呢,這個需要在第二步中解決。在解決第二步之前,你可能會有問題——這樣一來的話,如果有black蘋果(假設有black),那麼它的hashCode也變成了5了啊,和green一樣了。這個同樣靠第二步解決。
第二步:重寫equals()
我們已經知道,如果想讓兩個對象一樣,除了讓他們的hashCode值一樣外,還要讓他們的equals()函數返回true,兩個都符合才算一樣。所以第二步我們要重寫equals()函數,使“只要color一樣,兩個蘋果就是相同的”:
public boolean equals(Object obj) { if (!(obj instanceof Apple)) //首先類型要相同才有可能返回true return false; if (obj == this) //如果是自己跟自己比,顯然是同一個對象,返回true return true; return this.color.equals(((Apple) obj).color); //如果顏色相同,也返回true}
這樣一來,根據最後一句話,凡是顏色相同的蘋果,第二維也映射到同一個位置了(姑且這麼理解)。這樣一來,就可以根據顏色在hashMap裡尋找蘋果了。
把我們重寫過的hashCode()和equals()加入到之前的代碼中,便會輸出結果:10,即鍵a1所對應的值。
結語
感覺這篇文章涉及的內容還是相當基礎和重要的。文章到此也差不多可以結束了,另附上以前學習時記的筆記,感覺還是挺有用的,我自己的筆記自己看起來自然是毫無障礙的,不過實在不想整理了,就直接貼上來吧,大家將就將就看看吧,可以作為上面內容的嘮叨和補充。
附錄1
HashSet在儲存的時候(比如存的是字串),則存進去之後按照雜湊值排序(也就意味著遍曆的時候得到的順序不是我們添加的順序,即亂序),如果第二個對象和第一個Hash值一樣但是對象不一樣,則第二個會鏈在第一個後面。在添加對象的時候,add()返回boolean型,如果添加的對象相同(比如兩個相同的字串),則返回false,添加失敗。
HashSet如何保證元素的唯一性?
通過元素的方法——hashCode()和equals()來實現。
如果兩個元素的hashCode不同,直接就存了;
如果兩個元素的hashCode相同,則調用equals判斷是否為true,true則證明是同一個對象,就不存了,false的話證明是不同的對象,存之。
一旦自訂了對象,想要存進HashSet,則一定要覆寫hashCode()和equals()方法——
比如我們定義Person類,僅含有name,age兩個參數,規定:只要姓名和年齡相同,就斷定為“同一個人”,則不能存入HashSet,否則的話可以。
對於這種情況,如果我們new出來幾個人,其中存在名字和年齡相同的,則均會存入HashSet,原因就是這些對象是不同的對象,所佔記憶體不一樣,則通過hashCode()返回的雜湊值也都不一樣,所以理所當然的存入了HashSet。為了避免把“同一個人”存進HashSet,我們首先需要讓hashCode()針對“同一個人”返回相同的雜湊值,即覆寫hashCode()方法!
public int hashCode(){ return 110;}
這樣自然也可以,不過沒有必要讓所有的對象返回的雜湊值都一樣,只要“同一個人”的雜湊值一樣就行了,所以寫成這樣更好:
public int hashCode(){ return name.hashCode() + age;}
這樣的話“同一個人”返回的雜湊值就是相同的。不過這樣還是不夠完美,因為覆寫的這個hashCode()雖然會讓“相同的人”返回相同的雜湊值,但也可能會讓“不同的人”返回相同的雜湊值,比如兩個人name不同,age不同,但name的雜湊值加上age恰恰相同,這樣的話就坑爹了。為了避免這種現象,讓這種雜湊值恰巧撞上的機率進一步減小,我們寫成這樣會更好:
public int hashCode(){ return name.hashCode() + age * 19;}
最好是乘上一個素數之類的,可以大大降低“不同的人”的雜湊值撞上的機率。
通過以上hashCode()函數的覆寫,我們讓“相同的人”的雜湊值相同了,那麼接下來就要覆寫equals()函數!因為Java碰到雜湊值相同的情況之後,接下來要根據equals()函數判斷兩個對象是否相同,相同則不再存入HashSet,不同的話就存進去,且是鏈在和它雜湊值相同的對象上的(鏈成一串兒)。
附錄2:以下是TreeSet內容,和上面關係不大了
TreeSet可以對裡面的元素進行排序,比如如果對象是字串,則按照字典序排序。
如果要存自訂對象,需要讓自訂的對象具有比較性,這樣的話TreeSet才能將其按照一定的順序去排序。否則會報出異常。為了讓自訂對象能夠具有比較性,對象需要實現Comparable介面。
比如,有一個Person類,我們要new出來一些人放到TreeSet裡面,為了使對象具有可比性從而能夠存入TreeSet,我們規定按照對象的年齡排序。
首先,讓Person類實現Comparable介面,覆寫介面的compareTo()方法,其傳回值和c語言的strcmp()相同:
這樣的話,如果某兩個對象的年齡相同,則後來者將不會被存入TreeSet,因為後來者被認為和之前的那個是同一個對象。
所以我們對主關鍵字排序之後一定要對次關鍵字進行排序,只有所有的關鍵字都比較完畢還是返回0,我們才能認為兩個對象相同。
所以覆寫的compareTo()應該是這樣的:
public int compareTo(Object obj){ //傳進來的需要是Object類型,這一點要注意 if(!(obj instanceof Person)){ //傳進來的對象不對,直接拋出異常 throw new RuntimeException("Not The Same Kind Of Object!"); } Person p = (Person)obj; //將對象向下轉型 if(this.age < p.age) return -1; if(this.age > p.age) return 1; if(this.age == p.age) { return this.name.compareTo(p.name); //String類中覆寫過Comparable介面的空方法compareTo(),按照字典序對字串進行排序 }}
因此,如果你想讓TreeSet按照輸入順序存資料,而不是自動排序,可以這樣覆寫compareTo()方法:
public int compareTo(Object obj){ return 1;}
很簡單,不過缺陷就是“相同的人”也會被存進去。這個函數是完全按照輸入內容的順序不加以任何刪改原模原樣原順序存進TreeSet的。
同理,如果return -1就是輸入順序的逆序;如果return 0則只能存入第一個輸入的對象。
以上是TreeSet排序的第一種方法——讓元素自身具有比較性(讓類實現Comparable介面,覆寫compareTo()方法)。
不過這種方法有缺陷,比如我突然不想按照年齡排,想按照姓名排序,這就需要重新修改代碼了。
所以TreeSet排序的第二種方法——讓集合自身具有比較性(將自訂的比較子傳入Treeset的建構函式)。
比如我們現在要按照人名字典序排序:
首先建造一個比較子,實現Comparator介面,覆寫compare()方法(傳回值也和strcmp()一樣):
public PersonCompareMethod implements Comparator{ public int compare(Object obj1, Object obj2){ //傳入兩個Object對象 Person p1 = (Person)obj1; //向下轉型 Person p2 = (Person)obj2; int num = p1.getName().compareTo(p2.getName()); //直接比較兩個字串的字典序,因為String類已經覆寫過Comparable介面的空方法compareTo(),按照字典序對字串進行排序 if(num == 0){ return new Integer(p1.getAge()).compareTo(new Integer(p2.getAge())); //這一句話比較巧妙!!!本來我們是可以直接按照數字大小比較年齡這個次要關鍵字的,不過在比較的時候,我們換了一種比較高端的方法:建立了兩個Integer對象,因為Integer類裡面也有compareTo()方法,將數字按照字典序進行比較。當然是實現了Comparable介面並覆寫compareTo()方法才具有這樣的功能 } return num; }}
之後,將我們的構造器傳入TreeSet建立對象時調用的建構函式即可:
TreeSet ts = new TreeSet(new MyCompareMethod());
使用第二種方式的情況:
對象不具有比較性,或者是比較性並不是我們所需要的。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
Java中的equals()和hashCode()