HashMap與ArrayMap(和SparseArray)的比較與選擇
HashMap之外的Map實現
HashMap應該是java中使用最多的Map實現了,ArrayMap為Android SDK提供的另一個Map接口的實現。
SparseArray的實現思路和ArrayMap是一致的,所以捎上說一下
補充說明
ArrayMap在v4包中有兼容的實現,需要兼容低版本不要導錯包
android.util.ArrayMap
android.support.v4.util.ArrayMap
HashMap的實現
HashMap是通過數組+鏈表的形式存儲數據,內部有一個名為table的Node類型的數組用以存放數據,每一個Node都可以向后構成一個單向鏈表,用於在hash重復而key不相同時保存新的鍵值對
Node類的結構:
static class Node<K,V> implements Entry<K,V>{ final int hash; //key對象的hashCode值 final K key; //key對象 V value; //value對象 Node<K,V> next; //指向下一個Node對象的引用 }
- 1
- 2
- 3
- 4
- 5
- 6
HashMap通過Key對象的hashCode方法返回int型hash值,經過系列計算在數組中的下標。
下面分析一下hash->index的轉換過程
Key對象->table下標轉換
第一步,調用key對象的hashCode方法獲取int值
通過Key對象的hashCode方法,獲取int型的Hash值,如果key對象為null則為0。
這里就涉及到了HashMap和HashTable的一個區別:HashMap允許null Key而HashTable不允許。這是因為HashTable直接調用了Key對象的hashCode方法而缺少了null時的判斷。
將”HashMap通過key對象的hashCode方法獲取的int型hash值”起名為hash,后面提到hash均為此。
在Key對象為null時直接賦值為0進行第三步,不為0時多則進行第二步對hash修正
第二步,對hash修正
hash = hash ^ (hash>>>16)
- 1
將hash和自己的高16位xor。為什么多了這一步的操作的原因在下一步操作中說。
第三步,hash->數組下標轉換
如何保證計算出來的下標一定在數組長度范圍內?最簡單的方法就是hash%table.length,取余的結果一定在[0,table.length)區間內,這也是HashTable使用的方法。
但是計算機中除法和取余運算是最慢的,而位運算是最快的,所以HashMap使用位運算來轉換,這也是為什么HashMap的table長度一定是2n的原因。
我們知道2n對應的二進制是1后面n個零,以HashMap的table默認初始長度16為例,此時數組長度24對應二進制是
10000
減1可以得到
01111
index = 01111&hash,位與得到的結果index一定<=01111,也就是一定在[0,table.length)區間內。
HashMap用位運算實現了和HashTable取余同樣的效果(注意這里是等效不等價的),這也是除了同步鎖以外HashMap比HashTable效率高的另外一個原因。HashTable中hash值到index轉換是
index = (hash&0x7FFFFFFF)%table.length
- 1
使用符號位后和table.length取余,HashMap的位運算自然要比HashTable的取余運算效率高。
對第二步hash修正的說明
說回第二步中對hash的修正,hashCode方法返回的原始hash值存在一種可能,大部分1都在高位,此時數組又比較小的話,直接用原始hash值和table.length-1位與可能會丟掉太多1,導致hash大量碰撞,所以將高16位無符號右移並與低16位異或,這是為了讓高16位在數組長度比較小的情況下也能參與計算,降低hash碰撞概率
存取
獲取到數組下標后可以獲取對應位置的數組元素了,如果為空則表示不存在,可以直接存放新值。如果不為空就是Node鏈表的頭節點,此時需要遍歷Node對象,通過Key對象的equals檢查是否相符。增刪特性和鏈表相同不再細說。
ArrayMap的實現
ArrayMap內部通過兩個數組保存映射關系,其中int[] mHashes按大小順序保存Key對象hashCode值,Object[] mArray按mHashes的順序y用相鄰位置保存Key對象和Value對象。可以發現ArrayMap使用一個數組同時保存key和value對象,所以mArray長度一定是mHashes長度的2倍,通過兩個數組的初始化代碼也能看出
mHashes = new int[size]; mArray = new Object[size<<1];
- 1
- 2
ArrayMap相對於HashMap,無需為每個鍵值對創建Node對象,並且在數組中連續存放,這就是為什么ArrayMap相對HashMap要節省空間。
ArrayMap也是通過Key對象的hashCode方法返回int型hash值,通過一系列計算獲取對應在數組中的下標。下面分析ArrayMap中hash->index的轉換過程
Key對象->mArray下標轉換
第一步,調用key對象的hashCode方法獲取int值
通過Key對象的hashCode方法,獲取int型的Hash值,如果key對象為null則為0。這里和HashMap是完全一樣的。
和之前一樣,將”key對象的hashCode方法獲取的int型hash值“起名為hash
第二步,通過二分法查找獲取hash在mHashes數組中的下標index
mHashes中的hash值是按照有小到大的順序(自然排序)連續擺放的,通過binarySearch獲取對應hash的下標index,去mArray中查找鍵值對
第三步,mHashes下標查找mArray鍵值對
mHashes中的index*2即為mArray中的Key下標,index*2+1為Value的下標。由於存在hash碰撞的情況,而二分法查找到下標可能是多個連續相同hash值中的任意一個,所以此時需要用equals比對對命中的Key對象是否相符,不相符時,從當前index先向后再向前遍歷所有相同hash值。
存取
由於是用數組中連續位置存放的,數組各元素中沒有空余位置,空間占用更優。最好的情況時在最尾部增刪,如果在中間增刪則需要移動數組元素,這里和ArrayList原理相同不再細說。
index是通過二分法查找或者向后遍歷獲取的,插入時可以直接使用。
SparseArray
SparseArray和ArrayMap的實現原理是完全一樣的,都是通過二分法查找Key對象在Key數組中的下標來定位Value,SparseArray相比ArrayMap進一步優化空間提高性能。
SparseArray的目的是專門針對基本類型做優化,Key只能是可排序的基本類型,比如int型key的SparseArray,long型key的LongSparseArray,對於Value,除了泛型Value,對每種基本類型都有單獨的實現,比如SparseBooleanArray,SparseLongArray等等。
- 無需包裝
直接使用基本類型值,不需要包裝成對象。 - 無需hash,無需比對Key對象
直接使用基本類型值排序索引和判斷相等,無碰撞,無需調用hashCode方法,無需equals比較。 - 更小的內部數組
相比於ArrayMap,無需單獨的hash排序數組,內部只需等長的兩個數組分別存放Key和Value - 延遲刪除
對於移除操作,SparseArray並不是在每次remove操作直接移動數組元素,而是用一個刪除標記將對應key的value標記為已刪除,並標記需要回收,等待下次添加、擴容等需要移動數組元素的地方統一操作,進一步提升性能。 - 有序
所有鍵值對均是按照基本類型key的自然排序,支持下標訪問(keyAt方法和valueAt方法),迭代遍歷和數組相同
總結
1. 空間
HashMap的內部數組長度必須是2n,需要大一些降低碰撞概率(可以通過負載因子調節),數組元素是跳躍的,需要為鍵值對創建Node對象在碰撞時拉鏈。
ArrayMap則是通過犧牲性能換取空間,沒有2n限制,數組長度無需太長,size范圍內沒有閑置位置,無需為鍵值對創建Node對象。
2. 查找
HashMap在元素非常多時性能要高於ArrayMap。
HashMap直接通過hash值位運算計算出下標,ArrayMap需要通過二分法查找;hash碰撞時HashMap只需遍歷鏈表,ArrayMap需要分別向后向前遍歷數組。
3. 增刪
這個幾乎就是LinkedList和ArrayList的區別了
4. 擴容
這個是ArrayMap優於HashMap的地方。
HashMap的下標位置是和數組容量相關的,帶來一個問題,每次數組容量改變都需要重新計算所有鍵值對的下標,也就是rehash。而ArrayMap則沒有這個問題,只需要創建一個更大的數組,用System.arrayCopy把元素復制過去。
5. 遍歷
HashMap需要遍歷數組和數組中的每一個單向鏈表,並且數組元素是跳躍的;ArrayMap則只用遍歷一個連續的mArray數組即可
6.選擇
可以看出ArrayMap適合數量不多、對內存敏感、頻繁擴容的地方,而在元素比較多時HashMap更優