用作Map的鍵必須實現equals和hashCode方

Map有幾種基本實現,包括HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap。它們都有同樣的基本接口Map,但是行爲特性各不相同,這表現在效率、鍵值對的保存及呈現次序、對象的保存週期、映射表如何在多線程程序中工作和判定“鍵”等價的策略等方面。

對Map中鍵的要求:

1 任何鍵都必須具有一個equals()方法

2 如果鍵被用於散列Map,那麼它必須還具有恰當的hashCode()方法,不同的鍵可以生成相同的散列碼

3 如果鍵被用於TreeMap,那麼它必須實現Comparable。


HashMap代碼片段:
public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
 
        modCount++;
        addEntry(hash, key, value, i);
        return null;
}
 
public boolean  containsKey(Object key) {
        return getEntry(key) != null;
    }
 
   
final  Entry getEntry(Object key) {
        int hash = (key == null) ? 0 : hash(key.hashCode());
        for (Entry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }
 
public V get(Object key) {
        if (key == null)
            return getForNullKey();
        int hash = hash(key.hashCode());
        for (Entry 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;
    }


可以看到,HashMap使用equals()判斷當前的鍵是否與表中存在的鍵相同,HashMap是通過數組存儲鍵的信息,使用hashCode()計算鍵的信息並作爲存儲鍵信息的數組下標,這個數字就是散列碼,查找鍵對象的存儲位置時會用到hashCode()方法,具有相同散列碼的對象建立關係,存在在類似鏈表的結構中。通過鍵獲取對象時總是先根據散列碼查找實體對象,在調用equals()方法比較鍵對象是否相等。所以用作HashMap的鍵對象必須同時實現equals()方法和hashCode()方法,否則會導致找不到已經存儲在Map中的對象。get()方法與put()方法按照相同的方式計算索引。
 
看下面的代碼,首先會使用Groundhog和與之相關聯的Prediction填充HashMap,然後打印HashMap,最後使用標識數字爲3的Groundhog作爲鍵,查找與之對應的預報內容。
 
public classGroundhog {
  protectedintnumber;
  publicGroundhog(intn) { number= n; }
  publicString toString() {
    return"Groundhog #" + number;
  }
}
 
public classPrediction {
  privatestaticRandom rand= newRandom(47);
  privatebooleanshadow= rand.nextDouble() > 0.5;
  publicString toString() {
    if(shadow)
      return"Six more weeks of Winter!";
    else
      return"Early Spring!";
  }
}
public classSpringDetector {
  // Uses a Groundhog or class derived from Groundhog:
  publicstaticextendsGroundhog>
  voiddetectSpring(Class type) throws Exception {
    Constructor ghog = type.getConstructor(int.class);
    Map map =
      newHashMap();
    for(int i = 0; i < 10; i++)
      map.put(ghog.newInstance(i), new Prediction());
    print("map = " + map);
    Groundhog gh = ghog.newInstance(3);
    print("Looking up prediction for " + gh);
    if(map.containsKey(gh))
      print(map.get(gh));
    else
      print("Key not found: " + gh);
  }
  publicstaticvoidmain(String[] args) throws Exception {
    detectSpring(Groundhog.class);
  }
}


結果發現,無法通過數字3這個鍵找到其預報的內容,問題出在Groundhog自動的繼承自基類Object,所以這裏使用Object的hashCode()方法生成散列碼,而它默認是使用對象的地址計算散列碼。由Goundhog(3)生成的第一個實例的散列碼與由Goundhog(3)生成的第二個實例的散列碼是不同的,所以就找不到。


轉載自:http://blog.sina.com.cn/s/blog_494755fb0101g4kn.html


----------------------------------------------------------------------------------------------------

附:這兩條語句:int hash = hash(key.hashCode());   int i = indexFor(hash, table.length); 實現了哈希算法,算出了散列位置下標。


分析一下hash方法: 

Java代碼  收藏代碼
  1. h ^= (h >>> 20) ^ (h >>> 12);  
  2. return h ^ (h >>> 7) ^ (h >>> 4);  

假設key.hashCode()的值爲:0x7FFFFFFF,table.length爲默認值16。 
上面算法執行如下: 

 

得到i=15 

其中h^(h>>>7)^(h>>>4) 結果中的位運行標識是把h>>>7 換成 h>>>8來看。 

即最後h^(h>>>8)^(h>>>4) 運算後hashCode值每位數值如下: 
8=8 
7=7^8 
6=6^7^8 
5=5^8^7^6 
4=4^7^6^5^8 
3=3^8^6^5^8^4^7 
2=2^7^5^4^7^3^8^6 
1=1^6^4^3^8^6^2^7^5 
結果中的1、2、3三位出現重複位^運算 
3=3^8^6^5^8^4^7     ->   3^6^5^4^7 
2=2^7^5^4^7^3^8^6   ->   2^5^4^3^8^6 
1=1^6^4^3^8^6^2^7^5 ->   1^4^3^8^2^7^5 

算法中是採用(h>>>7)而不是(h>>>8)的算法,應該是考慮1、2、3三位出現重複位^運算的情況。使得最低位上原hashCode的8位都參與了^運算,所以在table.length爲默認值16的情況下面,hashCode任意位的變化基本都能反應到最終hash table 定位算法中,這種情況下只有原hashCode第3位高1位變化不會反應到結果中,即:0x7FFFF7FF的i=15。 


分析indexFor方法:

Java代碼  收藏代碼
  1. static int indexFor(int h, int length) {  
  2.        return h & (length-1);  
  3.    }  



首先算得key得hashcode值,然後跟數組的長度-1做一次“與”運算(&)。看上去很簡單,其實比較有玄機。比如數組的長度是2的4次方,那麼hashcode就會和2的4次方-1做“與”運算。很多人都有這個疑問,爲什麼hashmap的數組初始化大小都是2的次方大小時,hashmap的效率最高,我以2的4次方舉例,來解釋一下爲什麼數組大小爲2的冪時hashmap訪問的性能最高。 

         看下圖,左邊兩組是數組長度爲16(2的4次方),右邊兩組是數組長度爲15。兩組的hashcode均爲8和9,但是很明顯,當它們和1110“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個鏈表上,那麼查詢的時候就需要遍歷這個鏈表,得到8或者9,這樣就降低了查詢的效率。同時,我們也可以發現,當數組長度爲15的時候,hashcode的值會與14(1110)進行“與”,那麼最後一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味着進一步增加了碰撞的機率,減慢了查詢的效率!
 

 


          所以說,當數組長度爲2的n次冪的時候,不同的key算得得index相同的機率較小,那麼數據在數組上分佈就比較均勻,也就是說碰撞的機率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。 
          說到這裏,我們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼可以得知是16,爲什麼是16,而不是15,也不是20呢,看到上面annegu的解釋之後我們就清楚了吧,顯然是因爲16是2的整數次冪的原因,在小數據量的情況下16比15和20更能減少key之間的碰撞,而加快查詢的效率。 

所以,在存儲大容量數據的時候,最好預先指定hashmap的size爲2的整數次冪次方。就算不指定的話,也會以大於且最接近指定值大小的2次冪來初始化的,代碼如下(HashMap的構造方法中):

Java代碼  收藏代碼
  1. // Find a power of 2 >= initialCapacity  
  2.         int capacity = 1;  
  3.         while (capacity < initialCapacity)   
  4.             capacity <<= 1;  



總結: 
        本文主要描述了HashMap的結構,和hashmap中hash函數的實現,以及該實現的特性,同時描述了hashmap中resize帶來性能消耗的根本原因,以及將普通的域模型對象作爲key的基本要求。尤其是hash函數的實現,可以說是整個HashMap的精髓所在,只有真正理解了這個hash函數,纔可以說對HashMap有了一定的理解。


--------------------------------------------------------

java中hashcode的作用

摘錄自 http://blog.csdn.net/fenglibing/article/details/8905007

1、hashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap等,hashCode是用來在散列存儲結構中確定對象的存儲地址的;
2、如果兩個對象相同,就是適用於equals(Java.lang.Object) 方法,那麼這兩個對象的hashCode一定要相同;
3、如果對象的equals方法被重寫,那麼對象的hashCode也儘量重寫,並且產生hashCode使用的對象,一定要和equals方法中使用的一致,否則就會違反上面提到的第2點;
4、兩個對象的hashCode相同,並不一定表示兩個對象就相同,也就是不一定適用於equals(java.lang.Object) 方法,只能夠說明這兩個對象在散列存儲結構中,如Hashtable,他們“存放在同一個籃子裏”。




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