Java提升篇(二六)-----hashCode

6608733_100801001323_2_thumb2

      在前面三篇博文中LZ講解了(HashMapHashSetHashTable),在其中LZ不斷地講解他們的put和get方法,在這兩個方法中計算key的hashCode應該是最重要也是最精華的部分,因此下面LZ揭開hashCode的「神祕」面紗。html

hashCode的做用

      要想了解一個方法的內在原理,咱們首先須要明白它是幹什麼的,也就是這個方法的做用。在講解數組時(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對於一個對象的重要性

      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自己來進行區分。

hashCode與equals

      在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()有可能相等,也有可能不等。

      理清了上面的關係咱們就知道他們二者是如何配合起來工做的。先看下圖:

2014040701_thumb2

      整個處理流程是:

      一、判斷兩個對象的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());
    }
}

       運行結果以下:

2014040702_thumb

      從上圖能夠看出,程序調用四次hashCode方法,一次equals方法,其set的長度只有3。add方法運行流程徹底符合他們二者之間的處理流程。

      更多請關注:

      >>>>>>>>>Java提升篇(十三)------equals()

      >>>>>>>>>Java提升篇(二三)------HashMap

      >>>>>>>>>Java提升篇(二四)------HashSet

      >>>>>>>>>Java提升篇(二五)------HashTable

相關文章
相關標籤/搜索