深入解析Java對象的hashCode和hashCode在HashMap的底層數據結構的應用

java對象的比較

等號(==):

對比對象實例的內存地址(也即對象實例的ID),來判斷是否是同一對象實例;又可以說是判斷對象實例是否物理相等;

 

equals():

對比兩個對象實例是否相等。

當對象所屬的類沒有重寫根類Object的equals()方法時,equals()判斷的是對象實例的ID(內存地址),是否是同一對象實例;該方法就是使用的等號(==)的判斷結果,如Object類的源代碼所示:

public boolean equals(Object obj) {  
      return (this == obj);  
}  

當對象所屬的類重寫equals()方法(可能因爲需要自己特有的“邏輯相等”概念)時,equals()判斷的根據就因具體實現而異,有些類是需要比較對象的某些值或內容,如String類重寫equals()來判斷字符串的值是否相等。判斷邏輯相等。


hashCode():

計算出對象實例的哈希碼,並返回哈希碼,又稱爲散列函數。根類Object的hashCode()方法的計算依賴於對象實例的ID(內存地址),故每個Object對象的hashCode都是唯一的;當然,當對象所對應的類重寫了hashCode()方法時,結果就截然不同了。

 


二、Java的類爲什麼需要hashCode?---hashCode的作用,從Java中的集合的角度看。

  總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。你知道它們的區別嗎?前者集合內的元素是有序的,元素可以重複;後者元素無序,但元素不可重複。那麼這裏就有一個比較嚴重的問題了:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢?這就是 Object.equals方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後添加到集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。

    於是,Java採用了哈希表的原理。哈希算法也稱爲散列算法,當集合要添加新的元素時,將對象通過哈希算法計算得到哈希值(正整數),然後將哈希值和集合(數組)長度進行&運算,得到該對象在該數組存放的位置索引。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就表示發生衝突了,散列表對於衝突有具體的解決辦法,但最終還會將新元素保存在適當的位置。

    這樣一來,實際調用equals方法比較的次數就大大降低了,幾乎只需要一兩次。

    簡而言之,在集合查找時,hashcode能大大降低對象比較次數,提高查找效率!

 

 

 

三、Java  對象的equal方法和hashCode方法的關係

首先,Java對象相同指的是兩個對象通過eqauls方法判斷的結果爲true

 

Java對象的eqauls方法和hashCode方法是這樣規定的:

1、相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。

2、如果兩個對象的hashCode相同,它們並不一定相同。

 

關於第一點,

 想象一下,假如兩個Java對象A和B,A和B相等(eqauls結果爲true),但A和B的哈希碼不同,則A和B存入HashMap時的哈希碼計算得到的HashMap內部數組位置索引可能不同,那麼A和B很有可能允許同時存入HashMap,顯然相等/相同的元素是不允許同時存入HashMap,HashMap不允許存放重複元素

 

 關於第二點,

 也就是說,不同對象的hashCode可能相同;假如兩個Java對象A和B,A和B不相等(eqauls結果爲false),但A和B的哈希碼相等,將A和B都存入HashMap時會發生哈希衝突,也就是A和B存放在HashMap內部數組的位置索引相同這時HashMap會在該位置建立一個鏈接表,將A和B串起來放在該位置,顯然,該情況不違反HashMap的使用原則,是允許的。當然,哈希衝突越少越好,儘量採用好的哈希算法以避免哈希衝突。

 

 

 

四、深入解析HashMap類的底層數據結構

Map接口
  Map沒有繼承Collection接口,Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個 value。Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。

 

Hashtable類
  Hashtable繼承Map接口,實現一個key-value映射的哈希表。任何非空(non-null)的對象都可作爲key或者value。添加數據使用put(key, value),取出數據使用get(key),這兩個基本操作的時間開銷爲常數。

Hashtable通過initial capacity和load factor兩個參數調整性能。通常缺省的load factor 0.75較好地實現了時間和空間的均衡。增大load factor可以節省空間但相應的查找時間將增大,這會影響像get和put這樣的操作。
使用Hashtable的簡單示例如下,將1,2,3放到Hashtable中,他們的key分別是”one”,”two”,”three”:
    Hashtable numbers = new Hashtable();
    numbers.put(“one”, new Integer(1));
    numbers.put(“two”, new Integer(2));
    numbers.put(“three”, new Integer(3));
  要取出一個數,比如2,用相應的key:
    Integer n = (Integer)numbers.get(“two”);
    System.out.println(“two = ” + n);


HashMap
HashMap是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。

HashMap的數據結構:
HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。首先,HashMap類的屬性中定義了Entry類型的數組。Entry類實現java.ultil.Map.Entry接口,同時每一對key和value是作爲Entry類的屬性被包裝在Entry的類中。

如圖所示,HashMap的數據結構:




HashMap的部分源碼如下:

transient Entry[] table;  
   
static class Entry<K,V> implements Map.Entry<K,V> {  
    final K key;  
    V value;  
    Entry<K,V> next;  
    final int hash;  
    ……  
}

可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。table數組的元素是Entry類型的。每個 Entry元素其實就是一個key-value對,並且它持有一個指向下一個 Entry元素的引用,這就說明table數組的每個Entry元素同時也作爲某個Entry鏈表的首節點,指向了該鏈表的下一個Entry元素,這就是所謂的“鏈表散列”數據結構,即數組和鏈表的結合體。



HashMap的存取實現:


1) 添加元素:

當我們往HashMap中put元素的時候,先根據key的重新計算元素的hashCode,根據hashCode得到這個元素在table數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

HashMap的部分源碼如下:

public V put(K key, V value) {  
   // HashMap允許存放null鍵和null值。  
   // 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。  
   if (key == null)  
       return putForNullKey(value);  
   // 根據key的keyCode重新計算hash值。  
   int hash = hash(key.hashCode());  
   // 搜索指定hash值在對應table中的索引。  
   int i = indexFor(hash, table.length);  
   // 如果 i 索引處的 Entry 不爲 null,通過循環不斷遍歷 e 元素的下一個元素。  
   for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
       Object k;  
      // 如果發現 i 索引處的鏈表的某個Entry的hash和新Entry的hash相等且兩者的key相同,則新Entry覆蓋舊Entry,返回。  
       if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
           V oldValue = e.value;  
           e.value = value;  
           e.recordAccess(this);  
           return oldValue;  
       }  
   }  
   // 如果i索引處的Entry爲null,表明此處還沒有Entry。  
   modCount++;  
   // 將key、value添加到i索引處。  
   addEntry(hash, key, value, i);  
   return null;

2) 讀取元素:

有了上面存儲時的hash算法作爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

HashMap的部分源碼如下:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    int hash = hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
        e != null;
        e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
            return e.value;
    }
    return null;
}


3) 歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。

 

 

 

五、實現相等的對象必須具有相等的哈希碼

  如果相同的對象有不同的hashCode,對哈希表的操作會出現意想不到的結果(期待的get方法返回null),要避免這種問題,只需要牢記一條:要同時複寫equals方法和hashCode方法,而不要只寫其中一個。


同時複寫equals方法和hashCode方法,必須保證“相等的對象必須具有相等的哈希碼”,也就是當兩個對象通過equals()比較的結果爲true時,這兩個對象調用hashCode()方法生成的哈希碼必須相等。

 

如何保證相等,可以參考下面的方法:

複寫equals方法和hashCode方法時,equals方法的判斷根據和計算hashCode的依據相同。如String的equals方法是比較字符串每個字符,String的hashCode也是通過對該字符串每個字符的ASC碼簡單的算術運算所得,這樣就可以保證相同的字符串的hashCode相同且equals()爲真。

 

String類的equals方法的源代碼:

 /**
     * Compares this string to the specified object.  The result is {@code
     * true} if and only if the argument is not {@code null} and is a {@code
     * String} object that represents the same sequence of characters as this
     * object.
     *
     * @param  anObject
     *         The object to compare this {@code String} against
     *
     * @return  {@code true} if the given object represents a {@code String}
     *          equivalent to this string, {@code false} otherwise
     *
     * @see  #compareTo(String)
     * @see  #equalsIgnoreCase(String)
     */
    public boolean equals(Object anObject) {
	if (this == anObject) {
	    return true;
	}
	if (anObject instanceof String) {
	    String anotherString = (String)anObject;
	    int n = count;
	    if (n == anotherString.count) {
		char v1[] = value;
		char v2[] = anotherString.value;
		int i = offset;
		int j = anotherString.offset;
		while (n-- != 0) {
		    if (v1[i++] != v2[j++])
			return false;
		}
		return true;
	    }
	}
	return false;
    }

String類的hashCode方法計算hashCode的源代碼:

/**
     * Returns a hash code for this string. The hash code for a
     * <code>String</code> object is computed as
     * <blockquote><pre>
     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
     * </pre></blockquote>
     * using <code>int</code> arithmetic, where <code>s[i]</code> is the
     * <i>i</i>th character of the string, <code>n</code> is the length of
     * the string, and <code>^</code> indicates exponentiation.
     * (The hash value of the empty string is zero.)
     *
     * @return  a hash code value for this object.
     */
    public int hashCode() {
	int h = hash;
        int len = count;
	if (h == 0 && len > 0) {
	    int off = offset;
	    char val[] = value;

            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
        }
        return h;
    }




發佈了46 篇原創文章 · 獲贊 35 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章