?
java.util包中的集合類包含 Java 中某些最常用的類。最常用的集合類是 List 和 Map。List 的具體實現包括 ArrayList 和 Vector,它們是可變大小的列表,比較適合構建、存儲和操作任何類型對象元素列表。List 適用于按數值索引訪問元素的情形。
??????? Map 則提供了一個更通用的元素存儲方法。Map 集合類用于存儲元素對(稱作“鍵”和“值”),其中每個鍵映射到一個值。從概念上而言,您可以將 List 看作是具有數值鍵的 Map。而實際上,除了 List 和 Map 都在定義 java.util 中外,兩者并沒有直接的聯系。
??????? Map接口的實現類有很多,其中HashMap就是比較重要的一個實現,本文就以HashMap為主重點介紹。
??????? HashMap是基于哈希表的 Map 接口的實現。此實現提供所有可選的映射操作,并允許使用 null 值和 null 鍵。(除了非同步和允許使用 null 之外,HashMap 類與 Hashtable 大致相同。)此類不保證映射的順序,特別是它不保證該順序恒久不變。
??????? HashMap結合了ArrayList與LinkedList兩個實現的優點,,雖然HashMap并不會向List的兩種實現那樣在某項操作上性能較高,但是在基本操作(get 和 put)上具有穩定的性能。
?
??????? 首先從成員變量開始一點點的來了解HashMap和上述幾個概念。
?
????????1.HashMap的成員變量:
Java代碼
??
static?final?int?DEFAULT_INITIAL_CAPACITY?=?16;????static?final?int?MAXIMUM_CAPACITY?=?1?<<?30;????static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;????transient?Entry[]?table;????transient?int?size;????int?threshold;????final?float?loadFactor;????transient?volatile?int?modCount;?? ??????? HashMap其內部實現是一個Entry數組table,而Entry就是保存相應鍵值的實體。table數組默認大小為16,我們也可以在初始化時指定更大的值,但指定值必須為2的冪次方。
??????? 通過對ArrayList的學習了解到ArrayList其內部實現也是數組,當被添加的元素超出數組的容納極限時,ArrayList會對內部數組進行一次“擴容”,從而可以添加新的元素。
??????? 在HashMap中也有類似的概念,HashMap并不會像ArrayList一樣直到數組都滿了的情況下才去“擴容”,而是根據負載因子(load factor)來進行判斷。
??????? 舉例來說:HashMap實例中table數組的默認大小為16,負載因子為0.75,當添加元素個數大于等于12(16*0.75)時就會進行擴容。
??????? 所以說容量和負載因子直接影響著table數組是否擴容,什么時機擴容,進而影響這HashMap實例的性能。
??????? 當我們在初始化時可以指定HashMap實例的容量大小,當指定大小不為2的冪次方時,如下:
Java代碼
??
Map?map=new?HashMap(131);?? ??????? 請問初始化完成HashMap內table的長度是多少??答案為:256
??????? 其實只要打開HashMap的構造函數源代碼就明白為什么了,以下為源代碼:
Java代碼
??
public?HashMap(int?initialCapacity,?float?loadFactor)?{??????if?(initialCapacity?<?0)?? ????????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?? ????????????????+?initialCapacity);??????if?(initialCapacity?>?MAXIMUM_CAPACITY)?? ????????initialCapacity?=?MAXIMUM_CAPACITY;??????if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))?? ????????throw?new?IllegalArgumentException("Illegal?load?factor:?"?? ????????????????+?loadFactor);???????? ????int?capacity?=?1;?? ????while?(capacity?<?initialCapacity)?? ????????capacity?<<=?1;?? ??????this.loadFactor?=?loadFactor;?? ????threshold?=?(int)?(capacity?*?loadFactor);?? ????table?=?new?Entry[capacity];?? ????init();??}?? ??????? 關鍵在于這兩行:
Java代碼
??
while?(capacity?<?initialCapacity)??????capacity?<<=?1;?? ?
??????? 如果initialCapacity(指定大小)大于capacity(原或初始化大小)時,就會不斷循環進行位移賦值計算,相當于capacity=capacity *2.直至capacity 大于或等于我們指定的大小。如果指定的大小正好為2的N次冪時兩個值便會相等,進而終止計算;如果指定大小不符合條件時,capacity 就會是剛好大于指定大小的那個2的N次冪的數。
??????? 所以,在上面我們指定大小為131,大于131并且為2的的N次冪的數就為256,所以此時就會按256來初始化table.
?
????????2.Entry?元素
??????? 與LinkedList類似,HashMap也是采用Entry內部類來存儲實際元素信息,以下是Entry的源代碼(省略部分代碼):
Java代碼
??
static?class?Entry<K,?V>?implements?Map.Entry<K,?V>?{??????final?K?key;?? ????V?value;??????Entry<K,?V>?next;??????final?int?hash;?? }?? ??????? Entry中包括4個成員變量,其中key為鍵,value為值,next指向下一個節點元素,hash為hash值。Entry通過next屬性可以尋找到下一個節點的元素,進而通過遍歷就可以找到相應key下存儲的信息。
?
????????3.HashMap設置元素
??????? Map通過put方法來在Map實例中關聯指定值與指定鍵。如果該實例已經包含了一個該鍵的映射關系,則舊值被替換。
??????? 示例如下:
Java代碼
??
Map?map?=?new?HashMap();?? map.put("user1",?"小明");?? map.put("user2",?"小強");?? map.put("user3",?"小紅");?? System.out.println("user1:"?+?map.get("user1"));?? System.out.println("user2:"?+?map.get("user2"));?? System.out.println("user3:"?+?map.get("user3"));?? map.put("user2",?"小龍");?? System.out.println("user1:"?+?map.get("user1"));?? user1:小明??user2:小強??user3:小紅??user1:小明?? ??????? 首先,創建了一個HashMap的實例map,此時map實例中的table數組會默認初始化,創建一個長度為DEFAULT_INITIAL_CAPACITY=16的空數組。
??????? 然后,調用put方法將一對鍵、值(key,value)保存。當已存在Map實例中已存在指定key的映射時,會將新指定的value覆蓋原value。
??????? 與LIst的相關實現add方法一樣,HashMap的put方法是設置元素的入口,在put的過程中會進行一系列的判斷與操作,所以只有將put方法理解透徹后HashMap的內部結構與機制才會更加清晰。
??????? HashMap進行put操作時按以下步驟執行:
??????? 1)判斷key是否為空,如果為空則調用設置null的專有方法。
??????? 2)計算key的hash值。
??????? 3)通過hash與table數組的長度計算出該元素所要放置的數組下標。
??????? 4)遍歷該下標下的Entry元素鏈,如果找到與指定key相同的Entry則直接替換該Entry的value值并返回。
??????? 5)如果未找到則添加一個新元素至該下標下的元素鏈前端。
??????? 以下是一張官網上對于put操作流程的描述圖片,可以作為參考:
??????? 以下是put方法的源代碼,其中我已經加入了相關描述便于大家理解:
Java代碼
??
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<K,?V>?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;?? }?? ?
4.HashMap內部結構 通過對put方法的流程分析,我們基本已經了解HashMap其內部實現的機制與原理,那么來總結一下HashMap初始化及添加元素的過程(以默認值為例): (1) 初始化HashMap實例,初始化其內部數組table: Java代碼
??
this.loadFactor?=?DEFAULT_LOAD_FACTOR;threshold?=?(int)(DEFAULT_INITIAL_CAPACITY?*?DEFAULT_LOAD_FACTOR); table?=?new?Entry[DEFAULT_INITIAL_CAPACITY]; 此時table被初始化創建,長度為16。 (2) 當第一次put元素時,此時HashMap實例中并沒有添加任何元素,所以put方法會直接調用addEntry方法: Java代碼
??
Entry<K,V>?e?=?table[bucketIndex];??table[bucketIndex]?=?new?Entry<K,V>(hash,?key,?value,?e);?? 首先,會先獲取該下標(bucketIndex)下原Entry信息,因為table并未設置任何值,所以此時e為null。 然后,創建一個新的Entry實例,其next屬性指向e,并將此實例賦值給table[bucketIndex]。 (3) 當更新HashMap實例中已有key的value內容時: Java代碼
??
for?(Entry<K,V>?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;?? ????}??}?? ????????如果HashMap實例中已經put了該key則只需遍歷找到該節點Entry,更新其value并返回,所以更新已有key的操作不會調用addEntry方法。 (4) 此時HashMap實例的內部結構如下圖所示: ??????? HashMap采用此種存儲元素的方式是結合了ArrayList與LinkedList兩者的優點,雖然單純某項操作的性能上并不比二者之一高,但這種方式的好處就是存儲與獲取性能平穩,并不會出現劇烈波動的情況。
??????? 5.HashMap獲取元素 既然已經了解了HashMap的內部結構已經設置元素時的相關操作步驟,那么獲取元素其實也就比較容易理解了,首先根據指定的key去計算數組下標,然后遍歷該下標下的Entry鏈,最后返回。 以下是get方法的源代碼,與put方法的基本流程大致相同: Java代碼
??
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;?? }?? ??????? 6.HashMap移除元素 HashMap實現了Map接口的remove方法,所以可以通過remove方法移除已經添加的元素: Java代碼
??
Map?map?=?new?HashMap();?? map.put("user1",?"小明");?? map.put("user2",?"小強");?? map.put("user3",?"小紅");?? map.remove("user2");?? System.out.println("user1:"?+?map.get("user1"));?? System.out.println("user2:"?+?map.get("user2"));?? System.out.println("user3:"?+?map.get("user3"));?? user1:小明??user2:null?? user3:小紅?? ??????? 當主動調用remove方法時,會根據指定的key刪除該節點元素。 以下是remove方法的源代碼: Java代碼
??
public?V?remove(Object?key)?{??????Entry<K,?V>?e?=?removeEntryForKey(key);??????return?(e?==?null???null?:?e.value);?? }????final?Entry<K,?V>?removeEntryForKey(Object?key)?{??????int?hash?=?(key?==?null)???0?:?hash(key.hashCode());?? ????int?i?=?indexFor(hash,?table.length);?? ????Entry<K,?V>?prev?=?table[i];??????Entry<K,?V>?e?=?prev;????????while?(e?!=?null)?{?? ????????Entry<K,?V>?next?=?e.next;??????????Object?k;??????????if?(e.hash?==?hash?&&?((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))?{?? ????????????modCount++;??????????????size--;??????????????if?(prev?==?e)?? ????????????????table[i]?=?next;??????????????else?? ????????????????prev.next?=?next;??????????????e.recordRemoval(this);?? ????????????return?e;?? ????????}??????????prev?=?e;??????????e?=?next;??????}????????return?e;?? }?? ??????? remove方法調用了另一個方法removeEntryForKey,removeEntryForKey方法會循環遍歷指定下標下所有Entry節點元素,如果該key存在則修改該節點前一個節點的next指向,從而達到把該Entry節點移除Entry鏈的目的。 注意HashMap的remove操作一樣不會引起“減容”操作,這樣就不會影響性能。
??????? 7.HashMap的遍歷 通常情況下Map的使用者清楚該Map實例中有那些key,通過get(key)方法就可以直接將所有元素取出,但某些情況下這種做法產生的代碼將是一次性代碼,無法共用。 HashMap的遍歷通常采用以下幾種方式: 1)通過entrySet()方法可以獲取HashMap實例所有Entry的Set返回,所以通過entrySet方法返回并迭代可以獲取所有Entry元素: Java代碼
??
Map?map?=?new?HashMap();?? map.put("user1",?"小明");?? map.put("user2",?"小強");?? map.put("user3",?"小紅");?? Iterator?iter?=?map.entrySet().iterator();??while?(iter.hasNext())?{??????Map.Entry?entry?=?(Map.Entry)?iter.next();??????Object?key?=?entry.getKey();??????Object?value?=?entry.getValue();??????System.out.println("key:"?+?key?+?";value:"?+?value);?? ???? ????if?(key.toString().equals("user1"))?{?? ????????iter.remove();??????}?else?if?(key.toString().equals("user2"))?{?? ????????entry.setValue("小海");?? ????}????}??System.out.println(map.get("user1"));?? System.out.println(map.get("user2"));?? System.out.println(map.get("user3"));?? ??key:user2;value:小強??key:user1;value:小明??key:user3;value:小紅??null??小海??小紅?? ??????? 此種方式操作簡單,代碼量少,效率較高,且可以直接操作元素,是常用的手段之一。 2)Map還提供了keySet方法,用于返回所有key的Set形式,然后迭代此Set再通過get方法就可以獲取相應元素的value: Java代碼
??
Map?map?=?new?HashMap();?? map.put("user1",?"小明");?? map.put("user2",?"小強");?? map.put("user3",?"小紅");?? Iterator?iter?=?map.keySet().iterator();??while?(iter.hasNext())?{??????Object?key?=?iter.next();??????Object?value?=?map.get(key);??????System.out.println("key:"?+?key?+?";value:"?+?value);?? ???? ????if?(key.toString().equals("user1"))?{?? ????????iter.remove();??????}??}??System.out.println(map.get("user1"));?? System.out.println(map.get("user2"));?? System.out.println(map.get("user3"));?? ??key:user2;value:小強??key:user1;value:小明??key:user3;value:小紅??null??小強??小紅?? ??????? 此種方式先需要將所有key遍歷后返回,再通過get方法來獲取元素,如果單純需要操作Map實例中的個別節點元素時效率尚可,如果需要大規模獲取和修改時效率不如第一種。所以兩種方式選擇那種需要視情況而言,并沒有絕對。 3)通過values方法直接返回所有value: Java代碼
??
Map?map?=?new?HashMap();?? map.put("user1",?"小明");?? map.put("user2",?"小強");?? map.put("user3",?"小紅");?? String[]?names=?(String[])?map.values().toArray(new?String[map.size()]);?? for?(String?name?:?names){??????System.out.println(name);??}??Collection?nameArray?=??map.values();????Iterator?iter?=?nameArray.iterator();????while?(iter.hasNext())?{?????String?name=iter.next().toString();?????System.out.println(name);????}??小強??小明??小紅?? ??????? 此種方式簡單明了,適用于直接獲取所有value的情況,可以直接迭代或者轉換成數組,當直接顯示value的情況下比較適用。 HashMap的基本結構及內部實現原理至此已經比較清晰,下一篇著重來了解下HashMap其內部幾種算法的原理及相關性能。 http://286.iteye.com/blog/2187873
總結
以上是生活随笔為你收集整理的Map实现之HashMap(结构及原理)(转)的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。