Ø map中的每個成員方法由一個關鍵字(key)和一個值(value)構成。map接口不直接繼承于collection接口,因為它包裝的是一組成對的“鍵-值”對象的集合,而且在map接口的集合中也不能有重複的key出現,因為每個鍵隻能與一個成員元素相對應。
Ø map接口的子接口以及主要實作類有:
子接口:bindings、concurrentmap、concurrentnavigablemap、messagecontext、logicmessagecontext、navigablemap、soapmessagemap、sortedmap
實作類:abstractmap, attributes,authprovider, concurrenthashmap, enummap,concurrentskiplistmap,hashmap, hashtable, identityhashmap, linkedhashmap, printerstatereasons,properties,provider, renderinghints,
simplebindings, tabulardatasupport,treemap,uidefaults,weakhashmap
map接口中定義的方法清單:
Ø map中定義的方法說明:
在map接口中定義的通用方法并不是很多。
a) 添加和删除map中的某個元素
• put(k, v) : 将給定的“鍵-值”對放入到給定的map當中
• putall(map<? extends k, ? extends v) : 将指定的map中的“鍵-值”對放入到給定的map當中
• remove(object key) : 從該集合中移除指定的對象,并傳回對應的value
• clear() : 清空map中的所有對象
b) 查詢與map有關的資料
• int size() : 傳回此map中“鍵-值”對的個數
• boolean isempty() : 判斷此map中“鍵-值”對的個數是否為0
• boolean containskey(object key) : 測試此map中是否有該key
• boolean containsvalue(object value) : 測試此map中是否包含該value
• v get(object key) : 通過指定的key查詢map中對應的value
• collection<object value> values() : 取得map中所有的value
• set<object key> keyset() : 取得目前map中key的集合
• set<entry<k, v>> entryset() : 取得目前map中entry的集合
hashmap實作了map、clonemap、serializable三個接口,并且繼承自abstractmap類。
hashmap基于hash數組實作,若key的hash值相同則使用連結清單方式進行儲存,詳見hashset中的說明。我引用網上一個名詞叫“連結清單散列”來形容這樣的資料結構。
建立一個hashmap時會初始化一個大小為16,負載因子為0.75的空的hashmap。
<span style="font-size:10px;">1. /**
2. * the table, resized as necessary. length must always be a power of two.
3. */
4. transient entry[] table; </span>
那麼entry到底是怎麼實作的呢?
1. static class entry<k,v> implements map.entry<k,v> {
2. final k key;
3. v value;
4. entry<k,v> next;
5. final int hash;
6. ......
上面代碼其實告訴我們entry是一個結點,它持有下一個元素的引用,這樣就構成了一個連結清單。
那麼,整體上來說hashmap底層就是使用這樣一個資料結構來實作的。
我們提到使用hash,但是hash值如何與元素的存儲建立關系呢?(hash算法)
在資料結構課中我們學習過hash的簡單算法,就是給你一個hash因子,通過對該元素的hashcode簡單的求餘,來實作對其快速的定位和索引。
在hashmap中有這樣的代碼:
1. /**
2. * returns index for hash code h.
3. */
4. static int indexfor(int h, int length) {
5. return h & (length-1);
6. }
這個方法在hashmap中非常重要,凡是與查詢、添加、删除有關的方法中都有調用該方法,為什麼這麼短的一個代碼使用率這麼高?根據代碼注釋我們知道,這個方法是根據hashcode及目前table的長度(數組的長度,不是map的size)得到該元素應該存放的位置,或者在table中的索引。
現在我們需要看一下當資料量已經超過初始定義的負載因子時,hashmap如何處理?
在hashmap中當資料量很多時,并且已經達到了負載限度時,會重新做一次哈希,也就是說會再散列。調用的方法為resize(),并且java預設傳入的參數為2*table.length。先看一下jdk源碼:
1. /**
2. * rehashes the contents of this map into a new array with a
3. * larger capacity. this method is called automatically when the
4. * number of keys in this map reaches its threshold.
5. *
6. * if current capacity is maximum_capacity, this method does not
7. * resize the map, but sets threshold to integer.max_value.
8. * this has the effect of preventing future calls.
9. *
10. * @param newcapacity the new capacity, must be a power of two;
11. * must be greater than current capacity unless current
12. * capacity is maximum_capacity (in which case value
13. * is irrelevant).
14. */
15. void resize(int newcapacity) {
16. entry[] oldtable = table;
17. int oldcapacity = oldtable.length;
18. if (oldcapacity == maximum_capacity) {
19. threshold = integer.max_value;
20. return;
21. }
22.
23. entry[] newtable = new entry[newcapacity];
24. transfer(newtable);
25. table = newtable;
26. threshold = (int)(newcapacity * loadfactor);
27. }
28.
29. /**
30. * transfers all entries from current table to newtable.
31. */
32. void transfer(entry[] newtable) {
33. entry[] src = table;
34. int newcapacity = newtable.length;
35. for (int j = 0; j < src.length; j++) {
36. entry<k,v> e = src[j];
37. if (e != null) {
38. src[j] = null;
39. do {
40. entry<k,v> next = e.next;
41. int i = indexfor(e.hash, newcapacity);
42. e.next = newtable[i];
43. newtable[i] = e;
44. e = next;
45. } while (e != null);
46. }
47. }
48. }
看到這裡我們會發現resize(再哈希)的工作量是不是很大啊。再哈希是重建立一個指定容量的數組,然後将每個元素重新計算它要放的位置,這個工作量确實是很大的。
這裡就産生了一個很重要的問題,那就是怎麼讓哈希表的分布比較均勻,也就是說怎麼讓它即不會成為一個單連結清單(極限情況,每個key的hash值都集中到了一起),又不會使hash空間過大(導緻記憶體浪費)?
上面兩個問題一個是解決了怎麼計算hash值快速存取,一個是怎麼實作再哈希,何時需要再哈希。快速存取的前提是元素分布均勻,不至于集中到一點,再哈希是元素過于零散,導緻不斷的重新建構表。
那麼在第一個問題中我們看到了這樣一個代碼return h & (length-1);在第二個問題中我們說過内部調用傳入的值為2*table.length;并且預設情況下hashmap的大小為一個16的數字,除了預設構造提供大小為16的空間外,如果我們使用
public hashmap(int initialcapacity,float loadfactor)
上面的構造方法,我們會發現這樣的代碼:
1. // find a power of 2 >= initialcapacity
2. int capacity = 1;
3. while (capacity < initialcapacity)
4. capacity <<= 1;
5. ……
6. table = new entry[capacity];
也就是說當我們傳入1000時,它并沒有給我們構造一個容量為1000的哈希表,而是建構了一個容量為1024大小的哈希表。
從整體上我們發現一個問題,那就是無論什麼情況hashmap中哈希表的容量總是2的n次方的一個數。并且有這樣一個公式:
當length=2^n時,hashcode& (length-1) == hashcode % length
也就是這一點驗證了第一個問題,hash索引值的計算方法其實就是對哈希因子求餘。隻有大小為2的n次方時,那樣的計算才成立,是以hashmap為我們維護了一個這樣大小的一個哈希表。(位運算速度比取模運算快的多)
c) hashmap的使用方法:
我在很多代碼中都用到了hashmap,原因是首先它符合存儲關聯資料的要求,其次它的存取速度快,這是一個選擇的問題。
比較重要的是hashmap的周遊方法,keyset,entryset。
1. hashmap采用數組方式存儲key,value構成的entry對象,無容量限制。
2. hashmap基于key hash尋找entry對象存放到數組的位置,對于hash沖突采用連結清單的方式來解決。
3. hashmap在插入元素時可能要擴大數組的容量,擴大容量時對所有的資料要重新計算哈希和存放到新數組中。當元素個數size大于threshold擴容threshold = (int)(newcapacity* loadfactor);
4. hashmap保證數組的大小為2的指數大小。
5. hashmap非線程安全。
1. hashmap從java1.2後開始引進。hashtable從java1.0開始引入。hashtable一開始基于繼承陳舊的抽象類dictionary實作,後面也實作了map接口。hashmap基于map接口實作。
2. hashmap允許存放key為null,value為null。對于key為null時,hashmap首先擷取entry數組中的第一個entry對象,并基于entry對象的next周遊連結清單,當找到其中entry對象的key屬性為null時,更新其value值。如果沒有key屬性為null的entry,則調用addentry(int hash, k key, v value,
int bucketindex),參數為0,null,value,0,增加一個entry對象,增加時先擷取目前數組的第一個entry對象,記為e,然後建立一個key為null,value為傳入值得得entry對象,next為之前擷取的e。數組的第一個entry對象連結清單,賦為新建立的entry對象。由此,addentry連結清單倒序插入元素的。hashtable不允許key為null或value為null,否則會抛出nullpointerexception。這從put方法的源碼中很容易看出來,置于為什麼真麼限制,不明白?
3. hashmap是非線程安全;hashtable是線程安全的,其方法包含synchronized關鍵字實作線程安全。
4. hashmap的計算hash值方法如下:首先對key取其hashcode,然後通過如下計算:
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h>>> 4);
最後取index時:return h &(length-1);
hashtable計算hash值,直接去key的hashcode值,如下:
int hash =key.hashcode();
取index時:int index = (hash& 0x7fffffff) % tab.length;
5. hashmap和hashtable預設構造時,預設容量值不同。
hashmap預設數組大小為16,加載因子為0.75,重新hash門檻值為12.
hashtable預設數組大小為11,加載因子為0.75,重新hash門檻值為8.
6. hashmap和hashtable的數組擴容方式不同。
hashmap中的數組容量大小始終保證為2的指數。重新hash,擴充容量方式為,目前容量大小*2.
hashtable擴充容量方式為:int newcapacity = oldcapacity * 2 + 1;
7. 一些成員方法不同。
hashtable包含一些舊的方法,如contains方法。
面試中常會碰到。
linkedhashmap繼承自hashmap并且實作了map接口。和hashmap一樣,linkedhashmap允許key和value均為null。
于該資料結構和hashmap一樣使用到hash算法,是以它不能保證映射的順序,尤其是不能保證順序持久不變(再哈希)。
如果你想在多線程中使用,那麼需要使用collections.synchronizedmap方法進行外部同步。
linkedhashmap與hashmap的不同之處在于,linkedhashmap維護着運作于所有條目的雙向連結清單,此連結清單可以是插入順序或者通路順序。
對于linkedhashmap而言,它繼承與hashmap、底層使用哈希表與雙向連結清單來儲存所有元素。其基本操作與父類hashmap相似,它通過重寫父類相關的方法,來實作自己的連結清單特性。下面我們來分析linkedhashmap的源代碼:
linkedhashmap采用的hash算法和hashmap相同,但是它重新定義了數組中儲存的元素entry,該entry除了儲存目前對象的引用外,還儲存了其上一個元素before和下一個元素after的引用,進而在哈希表的基礎上又構成了雙向連結清單。看源代碼:
java代碼
1. /**
2. * 雙向連結清單的表頭元素。
3. */
4. private transient entry<k,v> header;
5.
6. /**
7. * linkedhashmap的entry元素。
8. * 繼承hashmap的entry元素,又儲存了其上一個元素before和下一個元素after的引用。
9. */
10.private static class entry<k,v> extends hashmap.entry<k,v> {
11. entry<k,v> before, after;
12. ……
13.}
通過源代碼可以看出,在linkedhashmap的構造方法中,實際調用了父類hashmap的相關構造方法來構造一個底層存放的table數組。如:
1. public linkedhashmap(int initialcapacity, float loadfactor) {
2. super(initialcapacity, loadfactor);
3. accessorder = false;
4. }
hashmap中的相關構造方法:
1. public hashmap(int initialcapacity, float loadfactor) {
2. if (initialcapacity < 0)
3. throw new illegalargumentexception("illegal initial capacity: " +
4. initialcapacity);
5. if (initialcapacity > maximum_capacity)
6. initialcapacity = maximum_capacity;
7. if (loadfactor <= 0 || float.isnan(loadfactor))
8. throw new illegalargumentexception("illegal load factor: " +
9. loadfactor);
10.
11. // find a power of 2 >= initialcapacity
12. int capacity = 1;
13. while (capacity < initialcapacity)
14. capacity <<= 1;
15.
16. this.loadfactor = loadfactor;
17. threshold = (int)(capacity * loadfactor);
18. table = new entry[capacity];
19. init();
20.}
我們已經知道linkedhashmap的entry元素繼承hashmap的entry,提供了雙向連結清單的功能。在上述hashmap的構造器
中,最後會調用init()方法,進行相關的初始化,這個方法在hashmap的實作中并無意義,隻是提供給子類實作相關的初始化調用。
linkedhashmap重寫了init()方法,在調用父類的構造方法完成構造後,進一步實作了對其元素entry的初始化操作。
1. void init() {
2. header = new entry<k,v>(-1, null, null, null);
3. header.before = header.after = header;
linkedhashmap并未重寫父類hashmap的put方法,而是重寫了父類hashmap的put方法調用的子方法void addentry(int hash, kkey, v value, int bucketindex) 和voidcreateentry(int hash, k key, v value, int bucketindex),提供了自己特有的雙向連結清單的實作。
1. void addentry(int hash, k key, v value, int bucketindex) {
2. // 調用create方法,将新元素以雙向連結清單的的形式加入到映射中。
3. createentry(hash, key, value, bucketindex);
4.
5. // 删除最近最少使用元素的政策定義
6. entry<k,v> eldest = header.after;
7. if (removeeldestentry(eldest)) {
8. removeentryforkey(eldest.key);
9. } else {
10. if (size >= threshold)
11. resize(2 * table.length);
12. }
1. void createentry(int hash, k key, v value, int bucketindex) {
2. hashmap.entry<k,v> old = table[bucketindex];
3. entry<k,v> e = new entry<k,v>(hash, key, value, old);
4. table[bucketindex] = e;
5. // 調用元素的addbrefore方法,将元素加入到哈希、雙向連結清單。
6. e.addbefore(header);
7. size++;
8. }
1. private void addbefore(entry<k,v> existingentry) {
2. after = existingentry;
3. before = existingentry.before;
4. before.after = this;
5. after.before = this;
6. }
linkedhashmap重寫了父類hashmap的get方法,實際在調用父類getentry()方法取得查找的元素後,再判斷當排序模式accessorder為true時,記錄通路順序,将最新通路的元素添加到雙向連結清單的表頭,并從原來的位置删除。由于的連結清單的增加、删除操作是常量級的,故并不會帶來性能的損失。
1. public v get(object key) {
2. // 調用父類hashmap的getentry()方法,取得要查找的元素。
3. entry<k,v> e = (entry<k,v>)getentry(key);
4. if (e == null)
5. return null;
6. // 記錄通路順序。
7. e.recordaccess(this);
8. return e.value;
9. }
1. void recordaccess(hashmap<k,v> m) {
2. linkedhashmap<k,v> lm = (linkedhashmap<k,v>)m;
3. // 如果定義了linkedhashmap的疊代順序為通路順序,
4. // 則删除以前位置上的元素,并将最新通路的元素添加到連結清單表頭。
5. if (lm.accessorder) {
6. lm.modcount++;
7. remove();
8. addbefore(lm.header);
9. }
10.}
linkedhashmap定義了排序模式accessorder,該屬性為boolean型變量,對于通路順序,為true;對于插入順序,則為false。
1. private final boolean accessorder;
一般情況下,不必指定排序模式,其疊代順序即為預設為插入順序。看linkedhashmap的構造方法,如:
1. public linkedhashmap(int initialcapacity,
2. float loadfactor,
3. boolean accessorder) {
4. super(initialcapacity, loadfactor);
5. this.accessorder = accessorder;
該哈希映射的疊代順序就是最後通路其條目的順序,這種映射很适合建構lru緩存。linkedhashmap提供了removeeldestentry(map.entry<k,v>eldest)方法,在将新條目插入到映射後,put和 putall将調用此方法。該方法可以提供在每次添加新條目時移除最舊條目的實作程式,預設傳回false,這樣,此映射的行為将類似于正常映射,即永遠不能移除最舊的元素。
1. protected boolean removeeldestentry(map.entry<k,v> eldest) {
2. return false;
3. }
此方法通常不以任何方式修改映射,相反允許映射在其傳回值的指引下進行自我修改。如果用此映射建構lru緩存,則非常友善,它允許映射通過删除舊條目來減少記憶體損耗。
例如:重寫此方法,維持此映射隻儲存100個條目的穩定狀态,在每次添加新條目時删除最舊的條目。
1. private static final int max_entries = 100;
2. protected boolean removeeldestentry(map.entry eldest) {
3. return size() > max_entries;
1. linkedhashmap繼承于hashmap,此連結清單定義了疊代順序,該疊代順序可以是插入順序或者是通路順序(按從近期通路最少到近期通路最多的順序)。預設情況下按插入順序通路,當accessorder設定為true時,按最後通路順序周遊資料。
2. linkedhashmap非線程安全。
3. linkedhashmap适合做lru緩存。
treemap是一個支援排序的基于紅黑樹(red-black tree)的<code>navigablemap</code>實作,非線程安全。treemap繼承abstractmap,實作navigablemap,navigablemap接口繼承sortedmap接口。此實作為 containskey、get、put 和 remove 操作提供受保證的 log(n) 時間開銷。
sortedmap提供關于鍵的總體排序的map。該映射是根據其鍵的自然順序進行排序的,或者根據通常在建立有序映射時提供的 comparator 進行排序。對有序映射的 collection 視圖(由 entryset、keyset 和 values 方法傳回)進行疊代時,此順序就會反映出來。
插入sortedmap的所有鍵都必須實作 comparable 接口(或者被指定的比較器接受)。另外,所有這些鍵都必須是可互相比較的:對有序映射中的任意兩個鍵 k1 和 k2 執行 k1.compareto(k2)(或 comparator.compare(k1, k2))都不得抛出 classcastexception。試圖違反此限制将導緻違反規則的方法或者構造方法調用抛出 classcastexception。
所有通用sortedmap實作類都應該提供 4 個“标準”構造方法:
1) void(無參數)構造方法,它建立一個空的有序映射,按照鍵的自然順序(實作comparable接口,方法compareto為自然比較方法,按此方法比較大小排序)進行排序。 2) 帶有一個 comparator 類型參數的構造方法,它建立一個空的有序映射,根據指定的比較器進行排序。
3) 帶有一個 map 類型參數的構造方法,它建立一個新的有序映射,其鍵-值映射關系與參數相同,按照鍵的自然順序進行排序。
4) 帶有一個 sortedmap 類型參數的構造方法,它建立一個新的有序映射,其鍵-值映射關系和排序方法與輸入的有序映射相同。無法保證強制實施此建議,因為接口不能包含構造方法。
sortedmap新增了一些成員方法,如下圖所示:
navigablemap<k,v>接口,擴充自 sortedmap,具有了針對給定搜尋目标傳回最接近比對項的導航方法。方法 lowerentry、floorentry、ceilingentry 和 higherentry 分别傳回與小于、小于等于、大于等于、大于給定鍵的鍵關聯的 map.entry 對象,如果不存在這樣的鍵,則傳回 null。類似地,方法 lowerkey、floorkey、ceilingkey 和 higherkey 隻傳回關聯的鍵。所有這些方法是為查找條目而不是周遊條目而設計的。
将comparator屬性指派為null。如果要控制treemap中元素的存儲順序,應使用帶comparator參數的構造器。
先判斷root是否為null,如果為null,則建立一個新的entry對象,并指派給root屬性。否則,首先判斷是否傳入了compatator實作,如果是,則基于紅黑樹的方式周遊,直到為樹節點null,使用傳入的comparator比較key的大小,如果找到相等的key則更新其值,若沒有找到……,如果comparator為null,則判斷key是否為null,如果是null,抛出nullpointerexception,對于key不是null時,取key對于的comparator進行紅黑樹的周遊和比較,與上述類似。
如果沒有找到相同的key,則建立一個新的entry對象,并将其parent設定為上面所尋找到的元素,并根據和parent key比較的情況設定parent的left和right屬性。最後對紅黑樹進行調整。
concurrenthashmap是線程安全的hashmap實作。支援擷取,查找時完全并發和更新時可調整并發的哈希表。擷取操作(包括 get)通常不會受阻塞。并發場景中(多個線程的情況下) concurrenthashmap比hashmap優秀很多。