?????? 最近一直特別忙,好不容易閑下來了。準備把HashMap的知識總結一下,很久以前看過HashMap源碼。一直想把集合類的知識都總結一下,加深自己的基礎。我覺的java的集合類特別重要,能夠深刻理解和應用這些集合類能夠讓自己寫的程序上一步臺階。
??????? 本文主要根據自己學習與使用HashMap來解析HashMap的源碼,深入到HashMap的內部結構和實現,增強自己的基礎知識。同時會借鑒網上的相關資料,深入理解HashMap.
HashMap的內部存儲結構
??????? 一提到HashMap我們就知道鍵值對,即一個鍵對應一個值。可能我們經常會使用HashMap,但是并不關注里面的內部實現。今天我們就來學習一下HashMap的內部實現。
???????? HashMap是一種以鍵值對存儲數據的容器,每個對象在java中都會有一個hashCode,HashMap正是借每個對象的HashCode來組織鍵值對的存儲,因為HashCode的特性,使得HashMap以非常快速和高效地地根據鍵值key進行數據的存取。鍵值對的具體實現在HashMap內部會封裝成一個Entry[] table,Entry[] table是鍵值對的表現形式。下圖為HashMap的存儲結構。HashMap中Value可以相同,但是鍵不可以相同.
?????? 上圖可以看出來HashMap是數組+鏈表的存儲結構,數組的每一個元素是一個鏈表的表頭。這樣的結構能夠綜合2個經常使用的數據結構的特點,數組查找、遍歷快,鏈表增加、刪除快。
HashMap的屬性
?????? static final int DEFAULT_INITIAL_CAPACITY = 16;//默認初始化加載容量,即table數組的長度。
?????? static final int MAXIMUM_CAPACITY = 1 << 30;//最大的容量。1左移30位,2的30次方:1073741824
?????? static final float DEFAULT_LOAD_FACTOR = 0.75f;//默認加載因子為0.75
?????? transient Entry[] table;//table數組,上圖的黃色部分。Entry為藍色部分
?????? transient int size;//HashMap存儲元素的數量。
?????? int threshold;//閥值? table的長度*加載因子(默認wei0.75)
?????? final float loadFactor;//加載因子
?????? transient volatile int modCount;//修改次數
?????? 注:如果用transient聲明一個實例變量,當對象存儲時,它的值不需要維持。換句話來說就? 是,用transient關鍵字標記的成員變量不參與序列化過程。
????????????? volatile為同步變量,保證每次都讀取的值是最新的。
HashMap的構造方法
??????? public HashMap() {//最常用的改造方法
????????????? this.loadFactor = DEFAULT_LOAD_FACTOR;//默認加載因子,0.75
????????????? threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//閥值??? 默認的容量*加載英子。12
????????????? table = new Entry[DEFAULT_INITIAL_CAPACITY];//table數組的默認為16,容量為16,切記容量不等于HashMap存儲的元素數量,容易混淆。
????????????? init();//初始化方法,里面是個空
????????? }
??????? 默認的構造方法開辟16個大小的空間。還有另外一個構造方法我們使用的比較少,當你覺的默認的HashMap的存儲空間浪費時(容量達到3/4時HashMap就會調用resize擴大為原來的2倍),可以使用下面一個。
?????? public HashMap(int initialCapacity, float loadFactor) {//初始化容量,加載因子
?????????????? if (initialCapacity < 0)//容量不能小于0
????????????????????? throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
????????????? if (initialCapacity > MAXIMUM_CAPACITY)//大于允許最大的容量時,設置未最大值
????????????????????? initialCapacity = MAXIMUM_CAPACITY;
???????????? if (loadFactor <= 0 || Float.isNaN(loadFactor))//加載因子小于大于0或者不是數字的時候,報非法加載因子異常
???????????? throw new IllegalArgumentException("Illegal load factor: " +
???????????? loadFactor);
???????????? // Find a power of 2 >= initialCapacity
??????????? int capacity = 1;
??????????? while (capacity < initialCapacity)//實際的開辟的空間要大于傳入的第一個參數的值。
??????????? capacity <<= 1;//這是一個重點,capacity才是容量。
??????????? this.loadFactor = loadFactor;//加載因子設置為傳入的值
??????????? threshold = (int)(capacity * loadFactor);//閾值
?????????? table = new Entry[capacity];//數組
?????????? init();
??????? }
???????? 此外還有2個構造方法基本上很少用到,感興趣的可以自己看看。HashMap最重要的方法是put和get方法,下面我們重點看看這2個方法。
HashMap的put方法分析
???????? public V put(K key, V value) {//很熟悉的方法
????????????? if (key == null)//如果key==null,直接設置空的
???????????????????? return putForNullKey(value);
????????????? int hash = hash(key.hashCode());//獲得hash值
????????????? int i = indexFor(hash, table.length);//根據hash值找到此鍵值對應該放在數組的第幾個位置。
????????????? 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;
???? }
????????? put方法會先判斷鍵是不是空,如果為空就調putForNullKey方法。方法如下:
????????? private V putForNullKey(V value) {
??????????????? for (Entry e = table[0]; e != null; e = e.next) {//獲得第一個鏈表,遍歷查找是否原來就存儲為null的鍵值對
???????????????? if (e.key == null) {//如果存在
?????????????????????? V oldValue = e.value;//記錄下老的值
?????????????????????? e.value = value;//把新值設置進去
?????????????????????? e.recordAccess(this);
?????????????????????? return oldValue;//返回老值
?????????????????? }
?????????????? }
???????????????? modCount++;
??????????????? addEntry(0, null, value, 0);//Key?為null,則將Entry放置到第一桶table[0]中
??????????????? return null;
???????? }
????????? 計算Hash值的方法如下:
?????????? static int hash(int h) {//根據特定的hashcode?重新計算hash值
????????????????????? h ^= (h >>> 20) ^ (h >>> 12);
?????????????????????? return h ^ (h >>> 7) ^ (h >>> 4);
????????????? }
??????????? static int indexFor(int h, int length) {//匹配到具體的桶當中,
?????????????????????? return h & (length-1);//相當于int i = hash %Entry[].length;得到i后,就是在Entry數組中的位置
???????????? }
?????? 整個put方法的流程如下:
??????? 首先判斷鍵是否為空,如果為空在判斷是否已經存在鍵為空的鍵值對,存在就更新值,不存在就新增;
??????? 如果鍵不為空,則獲取這個Key的hashcode值,根據此值確定鍵值對的存儲位置;遍歷所在桶中的Entry鏈表,查找其中是否已經有了以Key值為Key存儲的Entry對象,若已存在,定位到對應的Entry,其中的Value值更新為新的Value值;返回舊值;若不存在,則根據鍵值對 創建一個新的Entry對象,然后添加到這個桶的Entry鏈表的頭部。當前的HashMap的大小(即Entry節點的數目)是否超過了閥值,若超過了閥值(threshold),則增大HashMap的容量(即Entry[] table 的大小),并且重新組織內部各個Entry排列。
HashMap的get方法分析
???????? public V get(Object key) {
????????????? if (key == null)//如果鍵等于null,調用getForNullKey
???????????????????? return getForNullKey();
???????????? int hash = hash(key.hashCode());//獲得hash值,
???????????? for (Entry e = table[indexFor(hash, table.length)];
??????????????????? e != null;
???????????? e = e.next) {//indexfor計算存儲位置,根據位置遍歷鏈表
??????????????????? Object k;
??????????? if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
??????????????????? return e.value;//拿到對應的值
???????????? }
?????????????????? return null;
?????? }
???????? 如果鍵等于null,調用getForNullKey
???????? private V getForNullKey() {
???????????????? for (Entry e = table[0]; e != null; e = e.next) {//鍵為null的鍵值對存儲在數組的第一個元素,
???????????????? if (e.key == null)//如果存在則返回值
???????????????????????? return e.value;
??????????? }
??????????????????????? return null;//不存在返回null
}
?????? get方法比較簡單,比較容易理解。主要流程如下:
??????? 首先判斷鍵是否為空,如果為空在table[0]中取鍵為空的鍵值對,如果不存在為空的則返回null。
??????? 如果鍵不為空,則獲取這個Key的hashcode值,根據此值確定該鍵值對的存儲位置;遍歷所在桶中的Entry鏈表,查找其中是否已經有了以Key值為Key存儲的Entry對象,若已存在,定位到對應的Entry,獲得Value值;返回舊值;若不存在,則返回空。
HashMap的總結
??????? 本文主要講了HashMap的存儲結構以及基本屬性、構造方法,同時分析了比較常用的put和get方法。其他方法請讀者自行查看。在看HashMap的源碼時,我認為以下幾個方面比較重要,能夠理解到以下的幾點,搞定HashMap基本不成問題。
???????? HashMap的存儲結構,以及為什么要這樣進行存儲。
???????? HashMap的各個屬性的含義,以及其擴容的策略(擴容算法)。
???????? Hash值的計算.確定桶的位置。
???????? HashMap中的value可以相同,鍵不可以相同。
???????? HashMap是非線程安全的。