在前面三篇博文中LZ講解了(HashMap、HashSet、HashTable),在其中LZ不斷地講解他們的put和get方法,在這兩個方法中計算key的hashCode應該是最重要也是最精華的部分,因此下面LZ揭開hashCode的「神祕」面紗。html
要想了解一個方法的內在原理,咱們首先須要明白它是幹什麼的,也就是這個方法的做用。在講解數組時(java提升篇(十八)------數組),咱們提到數組是java中效率最高的數據結構,可是「最高」是有前提的。第一咱們須要知道所查詢數據的所在位置。第二:若是咱們進行迭代查找時,數據量必定要小,對於大數據量而言通常推薦集合。java
在Java集合中有兩類,一類是List,一類是Set他們之間的區別就在於List集合中的元素師有序的,且能夠重複,而Set集合中元素是無序不可重複的。對於List好處理,可是對於Set而言咱們要如何來保證元素不重複呢?經過迭代來equals()是否相等。數據量小還能夠接受,當咱們的數據量大的時候效率可想而知(固然咱們能夠利用算法進行優化)。好比咱們向HashSet插入1000數據,難道咱們真的要迭代1000次,調用1000次equals()方法嗎?hashCode提供瞭解決方案。怎麼實現?咱們先看hashCode的源碼(Object)。算法
public native int hashCode();
它是一個本地方法,它的實現與本地機器有關,這裏咱們暫且認爲他返回的是對象存儲的物理位置(實際上不是,這裏寫是便於理解)。當咱們向一個集合中添加某個元素,集合會首先調用hashCode方法,這樣就能夠直接定位它所存儲的位置,若該處沒有其餘元素,則直接保存。若該處已經有元素存在,就調用equals方法來匹配這兩個元素是否相同,相同則不存,不一樣則散列到其餘位置(具體狀況請參考(Java提升篇()-----HashMap))。這樣處理,當咱們存入大量元素時就能夠大大減小調用equals()方法的次數,極大地提升了效率。數組
因此hashCode在上面扮演的角色爲尋域(尋找某個對象在集合中區域位置)。hashCode能夠將集合分紅若干個區域,每一個對象均可以計算出他們的hash碼,能夠將hash碼分組,每一個分組對應着某個存儲區域,根據一個對象的hash碼就能夠肯定該對象所存儲區域,這樣就大大減小查詢匹配元素的數量,提升了查詢效率。緩存
hashCode重要麼?不重要,對於List集合、數組而言,他就是一個累贅,可是對於HashMap、HashSet、HashTable而言,它變得異常重要。因此在使用HashMap、HashSet、HashTable時必定要注意hashCode。對於一個對象而言,其hashCode過程就是一個簡單的Hash算法的實現,其實現過程對你實現對象的存取過程起到很是重要的做用。數據結構
在前面LZ提到了HashMap和HashTable兩種數據結構,雖然他們存在若干個區別,可是他們的實現原理是相同的,這裏我以HashTable爲例闡述hashCode對於一個對象的重要性。ide
一個對象勢必會存在若干個屬性,如何選擇屬性來進行散列考驗着一我的的設計能力。若是咱們將全部屬性進行散列,這一定會是一個糟糕的設計,由於對象的hashCode方法無時無刻不是在被調用,若是太多的屬性參與散列,那麼須要的操做數時間將會大大增長,這將嚴重影響程序的性能。可是若是較少屬相參與散列,散列的多樣性會削弱,會產生大量的散列「衝突」,除了不可以很好的利用空間外,在某種程度也會影響對象的查詢效率。其實這二者是一個矛盾體,散列的多樣性會帶來性能的下降。性能
那麼如何對對象的hashCode進行設計,LZ也沒有經驗。從網上查到了這樣一種解決方案:設置一個緩存標識來緩存當前的散列碼,只有當參與散列的對象改變時纔會從新計算,不然調用緩存的hashCode,這樣就能夠從很大程度上提升性能。大數據
在HashTable計算某個對象在table[]數組中的索引位置,其代碼以下:優化
int index = (hash & 0x7FFFFFFF) % tab.length;
爲何要&0x7FFFFFFF?由於某些對象的hashCode可能會爲負值,與0x7FFFFFFF進行與運算能夠確保index爲一個正數。經過這步我能夠直接定位某個對象的位置,因此從理論上來講咱們是徹底能夠利用hashCode直接定位對象的散列表中的位置,可是爲何會存在一個key-value的鍵值對,利用key的hashCode來存入數據而不是直接存放value呢?這就關係HashTable性能問題的最重要的問題:Hash衝突!
咱們知道衝突的產生是因爲不一樣的對象產生了相同的散列碼,假如咱們設計對象的散列碼能夠確保99.999999999%的不重複,可是有一種絕對且幾乎不可能遇到的衝突你是絕對避免不了的。咱們知道hashcode返回的是int,它的值只可能在int範圍內。若是咱們存放的數據超過了int的範圍呢?這樣就一定會產生兩個相同的index,這時在index位置處會存儲兩個對象,咱們就能夠利用key自己來進行判斷。因此具備相索引的對象,在該index位置處存在多個對象,咱們必須依靠key的hashCode和key自己來進行區分。
在Java中hashCode的實現老是伴隨着equals,他們是緊密配合的,你要是本身設計了其中一個,就要設計另一個。固然在多數狀況下,這兩個方法是不用咱們考慮的,直接使用默認方法就能夠幫助咱們解決不少問題。可是在有些狀況,咱們必需要本身動手來實現它,才能確保程序更好的運做。
對於equals,咱們必須遵循以下規則:
對稱性:若是x.equals(y)返回是「true」,那麼y.equals(x)也應該返回是「true」。
反射性:x.equals(x)必須返回是「true」。
類推性:若是x.equals(y)返回是「true」,並且y.equals(z)返回是「true」,那麼z.equals(x)也應該返回是「true」。
一致性:若是x.equals(y)返回是「true」,只要x和y內容一直不變,無論你重複x.equals(y)多少次,返回都是「true」。
任何狀況下,x.equals(null),永遠返回是「false」;x.equals(和x不一樣類型的對象)永遠返回是「false」。
對於hashCode,咱們應該遵循以下規則:
1. 在一個應用程序執行期間,若是一個對象的equals方法作比較所用到的信息沒有被修改的話,則對該對象調用hashCode方法屢次,它必須始終如一地返回同一個整數。
2. 若是兩個對象根據equals(Object o)方法是相等的,則調用這兩個對象中任一對象的hashCode方法必須產生相同的整數結果。
3. 若是兩個對象根據equals(Object o)方法是不相等的,則調用這兩個對象中任一個對象的hashCode方法,不要求產生不一樣的整數結果。但若是能不一樣,則可能提升散列表的性能。
至於二者之間的關聯關係,咱們只須要記住以下便可:
若是x.equals(y)返回「true」,那麼x和y的hashCode()必須相等。
若是x.equals(y)返回「false」,那麼x和y的hashCode()有可能相等,也有可能不等。
理清了上面的關係咱們就知道他們二者是如何配合起來工做的。先看下圖:
整個處理流程是:
一、判斷兩個對象的hashcode是否相等,若不等,則認爲兩個對象不等,完畢,若相等,則比較equals。
二、若兩個對象的equals不等,則能夠認爲兩個對象不等,不然認爲他們相等。
實例:
public class Person { private int age; private int sex; //0:男,1:女 private String name; private final int PRIME = 37; Person(int age ,int sex ,String name){ this.age = age; this.sex = sex; this.name = name; } /** 省略getter、setter方法 **/ @Override public int hashCode() { System.out.println("調用hashCode方法..........."); int hashResult = 1; hashResult = (hashResult + Integer.valueOf(age).hashCode() + Integer.valueOf(sex).hashCode()) * PRIME; hashResult = PRIME * hashResult + ((name == null) ? 0 : name.hashCode()); System.out.println("name:"+name +" hashCode:" + hashResult); return hashResult; } /** * 重寫hashCode() */ public boolean equals(Object obj) { System.out.println("調用equals方法..........."); if(obj == null){ return false; } if(obj.getClass() != this.getClass()){ return false; } if(this == obj){ return true; } Person person = (Person) obj; if(getAge() != person.getAge() || getSex()!= person.getSex()){ return false; } if(getName() != null){ if(!getName().equals(person.getName())){ return false; } } else if(person != null){ return false; } return true; } }
該Bean爲一個標準的Java Bean,從新實現了hashCode方法和equals方法。
public class Main extends JPanel { public static void main(String[] args) { Set<Person> set = new HashSet<Person>(); Person p1 = new Person(11, 1, "張三"); Person p2 = new Person(12, 1, "李四"); Person p3 = new Person(11, 1, "張三"); Person p4 = new Person(11, 1, "李四"); //只驗證p一、p3 System.out.println("p1 == p3? :" + (p1 == p3)); System.out.println("p1.equals(p3)?:"+p1.equals(p3)); System.out.println("-----------------------分割線--------------------------"); set.add(p1); set.add(p2); set.add(p3); set.add(p4); System.out.println("set.size()="+set.size()); } }
運行結果以下:
從上圖能夠看出,程序調用四次hashCode方法,一次equals方法,其set的長度只有3。add方法運行流程徹底符合他們二者之間的處理流程。
更多請關注:
>>>>>>>>>Java提升篇(十三)------equals()
>>>>>>>>>Java提升篇(二三)------HashMap
>>>>>>>>>Java提升篇(二四)------HashSet
>>>>>>>>>Java提升篇(二五)------HashTable