Map有幾種基本實現,包括HashMap,TreeMap,LinkedHashMap,WeakHashMap,ConcurrentHashMap,IdentityHashMap。它們都有同樣的基本接口Map,但是行爲特性各不相同,這表現在效率、鍵值對的保存及呈現次序、對象的保存週期、映射表如何在多線程程序中工作和判定“鍵”等價的策略等方面。
對Map中鍵的要求:1 任何鍵都必須具有一個equals()方法
2 如果鍵被用於散列Map,那麼它必須還具有恰當的hashCode()方法,不同的鍵可以生成相同的散列碼3 如果鍵被用於TreeMap,那麼它必須實現Comparable。
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方法:
- h ^= (h >>> 20) ^ (h >>> 12);
- 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方法:
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
首先算得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的構造方法中):
- // Find a power of 2 >= initialCapacity
- int capacity = 1;
- while (capacity < initialCapacity)
- 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,他們“存放在同一個籃子裏”。