1. 程式人生 > >java 中幾種map的儲存原理和記憶體佔用情況

java 中幾種map的儲存原理和記憶體佔用情況

Map,即對映,也稱為 鍵值對,有一個 Key, 一個 Value 。

比如 Groovy 語言中,  def  map = ['name' : 'liudehua', 'age' : 50 ] ,則 map[ 'name' ]  的值是 'liudehua'。 

那麼 Map 內部儲存是怎麼實現的呢?   下面慢慢講解.

一、 使用 拉鍊式儲存

這個以 Java 中的 HashMap 為例進行講解。   HashMap 的內部有個陣列 Entry[]  table, 這個陣列就是存放資料的。 

Entry 類的定義大致是 :

class Entry {

Object  key

Object  value

Entry next;

}

所以, Entry[]  table  的每個元素都是一個連結串列,即 HashMap 的內部儲存是 陣列 + 連結串列,即拉鍊式儲存。

當往 HaspMap 中 put(key,  value) 資料時,先進行  key.hashCode()  &  (table.length() - 1) ,得到一個小於 table.length() 的值, 稱為 index, 則這個新的 Entry 就屬於 table[index] 這個連結串列了 ( 如果連結串列還不存在,則把這個新的 Entry 作為連結串列的頭部了 );  然後開始從前往後遍歷  table[index] 這個連結串列,如果 key.equals( entry.key ), 那麼表示這個 key 已經有了舊值,則替換 value 的值即可;

否則,把這個新的 Entry 插入到 table[index] 連結串列的最前面.

以上就是 HashMap 的儲存方式介紹了, 而且可以知道,作為 HashMap 的 Key, 它的 hashCode() 和 equals() 方法都被使用了

二、陣列儲存

1.分散的陣列儲存

這個以 ThreadLocal  和 ThreadLocal.Values  類為例進行講解。 Values 類裡面有兩個變數, Object[]  table, 和 mask , table 就是儲存資料的陣列了,table 的長度是 2 的倍數 ,  mask 的值就是  table.length - 1 ;  這一點和 HashMap 的內部儲存很像。  不過 table 中的每個元素就不是連結串列了。

當往  Values 中進行 put(key, value) 時,先進行 key.hashCode & mask ,得到一個小於 table.length 的值,稱為 index (與上面的 HashMap 好像,哈哈), 然後去檢查 table[index] 的值,如果不存在,則在 table[index] 處放入 key, table[index + 1] 處放入 value; 如果已經存在了,且 key.equals( oldKey ) 不成立,即發生了衝突,那麼 index = index + 2 ( 此處是 + 2,因為 key ,value 兩個是挨著放的,一個元素佔倆坑 ) ; 往下一地方找找,如果再衝突,再找,直到找到一個可插入的地方,把 table[index] = key, table[index + 1] = value;  

有兩處需要注意:

key.hashCode() 必須是 2 的倍數, 否則 index 的值有可能為奇數,如此就可能發生衝突了.  可參考 ThreadLocal.hash 這個成員變數

table 內部的資料是分散著儲存的.

2.連續的陣列儲存

這個以 Android 中新增的 ArrayMap 為例進行分析( 據說沒 ArrayMap 是用來替換 HashMap 的哈 ),  ArrayMap 中有兩個主要變數, int[]  mHashes, Object[]  mArrays. 

mHashes 主要是存放 key 的 hash 值的, mArrays 是用來存放資料的,它也是奇數存放 key ,偶數存放 value , key 和 value 順序排列( 這個和 TheadLocal.value 中的 table 儲存方式很像 )。  mArrays 的長度是 mHashes 的 2 倍,畢竟 mArrays 是 key, value 都要存嘛~

mHashes 中存放 key 的 hash 值,是從小到大排列的,如果有多個 key 的 hash 值有一樣的,那麼就挨著排列

當往 ArrayMap 中進行 put(key, value) 時,先 int hash = key.hashCode, 然後通過二分查詢在 mHashes 中查詢 hash 的位置( 如果裡面有,就返回,如果無,就先找到最接近的位置,然後進行取反操作並返回 )  index,如果 index > 0 ,那麼再去 mArrays 中 2 * index 處獲取 key 值,比對兩個 key 是否 equals(), 如果不相等,那麼再分別向上、向下查詢附近相同 hash 值的 key ,看是否有 equals() 的 key, 若有,則替換,若無,則插入;    如果 index < 0 ,表示之前沒有相等 hash 的 key 插入過,那麼 index = ~index( 再次取反,就是個正數了,代辦要插入的位置), 再在 mHashes 的 index 處插入 hash, 在 mArrays 的 2 * index 處插入 key, 在 (2 * index ) + 1 處,插入 value . 

注意:

mHashes 和 mArrays 在插入新資料時,都需要把插入位置後面的資料向後移動一個單位,這個對於頻繁插入、刪除的動作來說消耗比較大.

key 的 hash 大小決定了插入的順序

3.以數字為 key 的陣列儲存

這類的 map 比較特殊,key 是數字型別。  這個以 Android 中新增的 SparseArray 進行分析。 SparseArray 中有兩個主要變數, int[]  mKeys  和  Object[]  mValues , mKeys 是存放 key 的,是個有序陣列,從小到大排列;  mValues 是與 mKeys 一一對應的 value 值集合.   mKeys 和 mValues 的長度是相等的。

當往  SparseArray 中 put(key, value) 時,先用二分查詢在 mKeys 中查詢 key 所在的位置 (如果找到,返回; 如果沒有找到,則找到它應該插入的位置,取反並返回) ,記為 index, index > 0 ,則直接在 mValues[index] 處替換 value; 如果 index < 0 ,則 index = ~index, 即取反, 然後在 mKeys 的 index 處插入 key , 在 mValues[index] 處插入 value ,之前的資料自 index 處後移一個單位。

注意:

mKeys 和 mArrays 的資料插入時,都是要進行資料移動的,對頻繁插入、刪除的 map 來說消耗很大.

最後了,對它們的優缺點做些對比。

HashMap : 記憶體佔用較大,增、刪的效率較高,改、查的效率一般

ThreadLocal.Values :  記憶體佔用一般,當資料量比較小時,增刪改查的效率高;資料量大時,增刪改查效率一般

ArrayMap: 記憶體佔用較小,改、查的效率高,增、刪的效率較低

SparseArray : 記憶體佔用較小,改、查的效率高,增、刪的效率低,且主鍵是數字

最後,我們不評判哪種儲存方式好,一切都要以實際情況實際分析,找出最符合的那種儲存,哈哈~