轉自:
HashMap所在Java集合的位置如下圖所示
1 、大致介紹一下java的集合體繫結構
List Set Map是這個集合體系中最主要的三個接口。
List、Set繼承自Collection接口。
Set不允許元素重複。HashSet和TreeSet是兩個主要的實現類。
List有序且允許元素重複。ArrayList、LinkedList和Vector是三個主要的實現類。
Map也屬於集合系統,但和Collection接口不同,AbstractMap實現了Map接口,HashMap繼承AbstractMap。SortedMap繼承Map接口,TreeMap繼承SortedMap。從圖中我們可知。
Map是key對value的映射集合,其中key是一個集合,key不能重複,但是value可以重複。HashMap、TreeMap和HashTable是三個主要實現類。
大概介紹了一下java的集合類,接下來主要介紹的是HashMap,HashMap在java集合類中的位置在圖中能看到。
2、HashMap位置
HashMap 繼承了AbstractMap,AbstractMap實現了Map接口,LinkedHashMap繼承了HashMap。
3、 什麼時候使用HashMap?
當你需要通過一個名字來獲取數據的時候就可以用Map,並且這個名字(也就是key)是不重複的,且在添加和刪除等情況下不需要線程安全,這時候我們就可以用HashMap。
比如當把用戶的信息存入list的時候,當你根據用戶id查詢某某學生名字時,可能需要遍歷,這時候用Map,直接通過key來找到value就可以了。
總之,需要鍵值對的時候,用map就可以了。
4 、HashMap使用code
code見:https://github.com/summerxhf/j2ee-demo/blob/master/HashMap-demo/src/main/java/HashMapDemo.java
5、 HashMap概述
HashMap 是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別不保證該順序恆久不變。(我們可以運行demo中的方法,插入順序和輸出順序並不是一個順序。)
6、 HashMap數據結構
在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用)。HashMap實際上是一個“鏈表散列”的數據結構,即數組和鏈表的結合體。
一維數組
鏈表
所以HashMap結構如下圖
所以HashMap底層就是一個數組結構,數組中的每一項又是存放的鏈表的頭結點。當新建一個HashMap的時候,就會初始化一個數組。
當new一個HashMap,內部代碼如下所示:
對於任何一個數組,在初始化建立的時候,都會涉及到建立數組的大小,數組長度是否夠用?能否自動擴充數組容量呢?這些問題。
下面是初始化數組時的一下參數定義code
- /**
- * The defaultinitial capacity - MUST be a power of two.
- */
- static final int DEFAULT_INITIAL_CAPACITY = 16;// 默認初始容量爲16,必須爲2的冪
- /**
- * The maximumcapacity, used if a higher value is implicitly specified
- * by either of the constructors with arguments.
- * MUST be apower of two <= 1<<30.
- */
- static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量爲2的30次方
- /**
- * The loadfactor used when none specified in constructor.
- */
- static final floatDEFAULT_LOAD_FACTOR = 0.75f;// 默認加載因子0.75
- /**
- * The table,resized as necessary. Length MUST Always be a power oftwo.
- */
- transientEntry<K,V>[] table;// Entry數組,哈希表,長度必須爲2的冪
- /**
- * The number ofkey-value mappings contained in this map.
- */
- transient int size;// 已存元素的個數
- /**
- * The next sizevalue at which to resize (capacity * load factor).
- * @serial
- */
- int threshold;// 下次擴容的臨界值,size>=threshold就會擴容
- /**
- * The loadfactor for the hash table.
- *
- * @serial
- */
- final float loadFactor;// 加載因子
在newHashMap的時候,構造方法如下:
構造方法摘要 |
|
HashMap() 構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。 |
|
HashMap(int initialCapacity) 構造一個帶指定初始容量和默認加載因子 (0.75)的空 HashMap。 |
|
HashMap(int initialCapacity, float loadFactor) 構造一個帶指定初始容量和加載因子的空 HashMap。 |
|
HashMap(Map<?extendsK,? extendsV> m) 構造一個映射關係與指定 Map 相同的 HashMap。 |
我們常用的沒有參數的構造方法,代碼如下。
//構造一個具有默認初始容量 (16)和默認加載因子 (0.75) 的空 HashMap。
- public HashMap() {
- this.loadFactor = DEFAULT_LOAD_FACTOR;
- threshold = (int)(DEFAULT_INITIAL_CAPACITY* DEFAULT_LOAD_FACTOR);
- table = new Entry[DEFAULT_INITIAL_CAPACITY];
- init();
- }
table爲Entry數組,怎麼理解這個Entry?map爲地圖,Entry可以理解爲地圖中的各個節點,登記處。在new一個HashMap的時候,默認會初始化16個Entry,加載因子是,當數組中的個數超出了加載因子與當前容量的乘積時,就會通過調用rehash方法將容量翻倍。例如默認的擴容因子爲0.75, 則擴充的臨界值爲16* 0.75 = 12, 也就是map中存放超過12個key value映射時,就會自動擴容。
7 、HashMap初始化之後
new完一個HashMap後,進行put值,put的代碼如下:
//在此映射中關聯指定值與指定鍵。如果該映射以前包含了一個該鍵的映射關係,則舊值被替換,並返回舊值。
- public V put(K key, V value) {
- // 如果key爲null使用putForNullKey來獲取
- if (key == null)
- return putForNullKey(value);
- // 使用hash函數預處理hashCode
- int hash = hash(key.hashCode());
- // 獲取對應的索引
- int i = indexFor(hash, table.length);
- // 得到對應的hash值的桶,如果這個桶不是,就通過next獲取下一個桶
- for (Entry<K,V> e = table[i]; e != null;e = e.next) {
- Object k;
- // 如果hash相同並且key相同
- if (e.hash== hash && ((k = e.key) == key || key.equals(k))) {
- // 獲取當前的value
- V oldValue = e.value;
- // 將要存儲的value存進去
- e.value = value;
- e.recordAccess(this);
- // 返回舊的value
- return oldValue;
- }
- }
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
// key爲null怎麼放value
- private V putForNullKey(V value) {
- // 遍歷table[0]的所有桶
- for (Entry<K,V> e = table[0]; e != null;e = e.next) {
- // 如果key是null
- if (e.key== null) {
- // 取出oldValue,並存入value
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- // 返回oldValue
- return oldValue;
- }
- }
- modCount++;
- addEntry(0, null, value, 0);
- return null;
- }
//預處理hash值,避免較差的離散hash序列,導致桶沒有充分利用
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>>4);
}
//返回對應hash值得索引 ,h爲key的hashCode處理後的值,length爲table中Entry數組大小。
- static int indexFor(int h, int length) {
- /*****************
- * 由於length是2的n次冪,所以h &(length-1)相當於h % length。
- * 對於length,其2進製表示爲1000...0,那麼length-1爲0111...1。
- * 那麼對於任何小於length的數h,該式結果都是其本身h。
- * 對於h = length,該式結果等於0。
- * 對於大於length的數h,則和0111...1位與運算後,
- * 比0111...1高或者長度相同的位都變成0,
- * 相當於減去j個length,該式結果是h-j*length,
- * 所以相當於h % length。
- * 其中一個很常用的特例就是h & 1相當於h % 2。
- * 這也是爲什麼length只能是2的n次冪的原因,爲了優化。
- */
- return h & (length-1);
- }
8、瞭解HashMap其他構造函數的源碼
https://github.com/summerxhf/j2ee-demo/blob/master/HashMap-demo/src/main/java/HashMap.java
我們可以看到,我們在put的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(方法 indexFor),如果數組中的位置上已經有其他元素了,那麼這個元素將以鏈表的形式存放,在鏈表頭中加入新的,最先put的key的value 放在鏈尾。如果該數組位置上沒有元素,則直接將該元素放到改位置上。
9、 一些其他疑問
9.1、對於,put key爲null的值呢?
如果key爲null的時候,方法putForNullKey告訴我們答案。
key爲null的時候,value也可以不爲null。不過在 addEntry(0, null, value, 0);的時候,存放的hash值,以及數組的下標值爲0,key值爲null。
如下圖所示,每一行鏈表中,Entry的key是一個。Entry中有key和value ,以及鏈表連接指向。
9.2、 有人會問到底啥事hashCode?
其實就是經過一系列的數學運算,移位運算得到一個數,就成爲了hashCode。不解釋,看源碼哦.
9.3、 Entry的數組的大小,也就是table的大小,爲什麼必須是2的冪次方?
HashMap的結構是數組+單鏈表結構,我們希望元素是均勻分配的,最理想的效果是,Entry中的每個位置都只有應元素,也就是鏈表的頭結點,就是鏈表的尾節點,這樣查詢效率最高,不需要遍歷鏈表,有而不需要進行equals比較key,而且利用率最大 ,%取模運算 哈希值%table容量=數組下標,而代碼中這樣實現的h & (length-1),當length總是2的n次方時,h &(length-1)運算等價於對length取模,也就是h%length,但是&比%的效率要高。
9.4、 HashMap線程不安全,那多線程下使用如何做呢?
1包裝一下
2 Map m =Collections.synchronizedMap(newHashMap(...));
3使用java.util.HashTable,效率最低
4使用java.util.concurrent.ConcurrentHashMap,相對安全,效率較高
接下來一一說明Map接口的其他實現 。
總結
從如何使用上,key value映射時,HashMap的原理上,一維數組+單鏈表結構,大概瞭解了他,總之,追本溯源,很好的瞭解他,才能很好的控制他。