HashMap也是咱們使用很是多的Collection,它是基於哈希表的 Map 接口的實現,以key-value的形式存在。在HashMap中,key-value老是會當作一個總體來處理,系統會根據hash算法來來計算key-value的存儲位置,咱們老是能夠經過key快速地存、取value。html
HashMap.java源碼分析: 三個構造函數: HashMap():默認初始容量capacity(16),默認加載因子factor(0.75) HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。 HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap。java
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//構建自定義初始容量的構造函數,默認加載因子0.75的HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//構造一個帶指定初始容量和加載因子的空 HashMap
public HashMap(int initialCapacity, float loadFactor) {
...
...
}
複製代碼
HashMap內部是使用一個默認容量爲16的數組來存儲數據的,而數組中每個元素卻又是一個鏈表的頭結點,因此,更準確的來講,HashMap內部存儲結構是使用哈希表的拉鍊結構(數組+鏈表),如圖: 這種存儲數據的方法叫作拉鍊法 面試
且每個結點都是Entry類型,那麼Entry是什麼呢?咱們來看看HashMap中Entry的屬性:算法
final K key; //key值
V value; //value值
HashMapEntry<K,V> next;//next下一個Entry
int hash;//key的hash值
複製代碼
put(key,value);數組
public V put(K key, V value) {
if (table == EMPTY_TABLE) {//判斷table空數組,
inflateTable(threshold);//建立數組容量爲threshold大小的數組,threshold在HashMap構造函數中賦值initialCapacity(指定初始容量);
}
//當key爲null,調用putForNullKey方法,保存null與table第一個位置中,這是HashMap容許key爲null的緣由
if (key == null)
return putForNullKey(value);
int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); //計算key的hash值
int i = indexFor(hash, table.length); //計算key hash 值在 table 數組中的位置
//從i出開始迭代 e,找到 key 保存的位置
for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判斷該條鏈上是否有hash值相同的(key相同)
//若存在相同,則直接覆蓋value,返回舊value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;//舊值 = 新值
e.value = value;
e.recordAccess(this);
return oldValue;//返回覆蓋後的舊值
}
}
//修改次數增長1
modCount++;
//將key、value添加至i位置處
addEntry(hash, key, value, i);
return null;
}
複製代碼
put過程分析:這篇文章www.cnblogs.com/chenssy/p/3…總結的能夠。bash
put過程結論: 當咱們想一個HashMap中添加一對key-value時,系統首先會計算key的hash值,而後根據hash值確認在table中存儲的位置。若該位置沒有元素,則直接插入。不然迭代該處元素鏈表並依此比較其key的hash值。若是兩個hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),則用新的Entry的value覆蓋原來節點的value。若是兩個hash值相等但key值不等 ,則將該節點插入該鏈表的鏈頭。app
void addEntry(int hash, K key, V value, int bucketIndex) {
//獲取bucketIndex處的Entry
Entry<K, V> e = table[bucketIndex];
//將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
//若HashMap中元素的個數超過極限了,則容量擴大兩倍
if (size++ >= threshold)
resize(2 * table.length);
}
複製代碼
這個方法中有兩點須要注意:框架
一是鏈的產生。這是一個很是優雅的設計。系統老是將新的Entry對象添加到bucketIndex處。若是bucketIndex處已經有了對象,那麼新添加的Entry對象將
指向原有的Entry對象,造成一條Entry鏈,可是若bucketIndex處沒有Entry對象,也就是e==null,那麼新添加的Entry對象指向null,也就不會產生Entry鏈了。
2、擴容問題。
隨着HashMap中元素的數量愈來愈多,發生碰撞的機率就愈來愈大,所產生的鏈表長度就會愈來愈長,這樣勢必會影響HashMap的速度,爲了保證HashMap的效率,系統必需要在某個臨界點進行擴容處理。該臨界點在當HashMap中元素的數量等於table數組長度*加載因子。可是擴容是一個很是耗時的過程,由於它須要從新計算這些數據在新table數組中的位置並進行復制處理。因此若是咱們已經預知HashMap中元素的個數,那麼預設元素的個數可以有效的提升HashMap的性能。
複製代碼
讀取實現:get(key) 相對於HashMap的存而言,取就顯得比較簡單了。經過key的hash值找到在table數組中的索引處的Entry,而後返回該key對應的value便可。函數
public V get(Object key) {
// 若爲null,調用getForNullKey方法返回相對應的value
if (key == null)
return getForNullKey();
// 根據該 key 的 hashCode 值計算它的 hash 碼
int hash = hash(key.hashCode());
// 取出 table 數組中指定索引處的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key與查找的key相同,則返回相對應的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
複製代碼
在不斷的向HashMap裏put數據時,當達到必定的容量限制時(這個容量知足這樣的一個關係時候將會擴容:HashMap中的數據量>容量*加載因子,而HashMap中默認的加載因子是0.75),HashMap的空間將會擴大;擴大以前容量的2倍 :resize(newCapacity)源碼分析
int newCapacity = table.length;//賦值數組長度
newCapacity <<= 1;//x2
if (newCapacity > table.length)
resize(newCapacity);//調整HashMap大小容量爲以前table的2倍
複製代碼
這也就是重點所在,爲何在Android上須要使用SparseArray和ArrayMap代替HashMap,主要緣由就是Hashmap隨着數據不斷增多,達到最大值時,須要擴容,並且擴容的大小是以前的2倍.
SparseArray.java 源碼 SparseArray比HashMap更省內存,在某些條件下性能更好,主要是由於它避免了對key的自動裝箱(int轉爲Integer類型),它內部則是經過兩個數組來進行數據存儲的,一個存儲key,另一個存儲value,爲了優化性能,它內部對數據還採起了壓縮的方式來表示稀疏數組的數據,從而節約內存空間,咱們從源碼中能夠看到key和value分別是用數組表示:
private int[] mKeys;//int 類型key數組
private Object[] mValues;//value數組
複製代碼
構造函數: SparseArray():默認容量10; SparseArray(int initialCapacity):指定特定容量的SparseArray
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {//判斷傳入容量值
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {//不爲0初始化key value數組
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;//mSize賦值0
}
複製代碼
從上面建立的key數組:SparseArray只能存儲key爲int類型的數據,同時,SparseArray在存儲和讀取數據時候,使用的是二分查找法;
/**
* 二分查找,中間位置的值與須要查找的值循環比對
* 小於:範圍從mid+1 ~ h1
* 大於:範圍從0~mid-1
* 等於:找到值返回位置mid
*/
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
複製代碼
SparseArray的put方法:
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//二分查找數組mKeys中key存放位置,返回值是否大於等於0來判斷查找成功
if (i >= 0) {//找到直接替換對應值
mValues[i] = value;
} else {//沒有找到
i = ~i;//i按位取反獲得非負數
if (i < mSize && mValues[i] == DELETED) {//對應值是否已刪除,是則替換對應鍵值
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {//當mGarbage == true 而且mSize 大於等於key數組的長度
gc(); //調用gc回收
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//最後將新鍵值插入數組,調用 GrowingArrayUtils的insert方法:
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
複製代碼
下面進去看看 GrowingArrayUtils的insert方法有什麼擴容的;
public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {//小於數組長度
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
//大於數組長度須要進行擴容
T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
growSize(currentSize));//擴容規則裏面就一句三目運算:currentSize <= 4 ? 8 : currentSize * 2;(擴容2倍)
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
複製代碼
SparseArray的get(key)方法:
public E get(int key) {
return get(key, null);//調用get(key,null)方法
}
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//二分查找key
if (i < 0 || mValues[i] == DELETED) {//沒有找到,或者已經刪除返回null
return valueIfKeyNotFound;
} else {//找到直接返回i位置的value值
return (E) mValues[i];
}
}
複製代碼
SparseArray在put添加數據的時候,會使用二分查找法和以前的key比較當前咱們添加的元素的key的大小,而後按照從小到大的順序排列好,因此,SparseArray存儲的元素都是按元素的key值從小到大排列好的。 而在獲取數據的時候,也是使用二分查找法判斷元素的位置,因此,在獲取數據的時候很是快,比HashMap快的多,由於HashMap獲取數據是經過遍歷Entry[]數組來獲得對應的元素。
SparseArray應用場景:
雖然說SparseArray性能比較好,可是因爲其添加、查找、刪除數據都須要先進行一次二分查找,因此在數據量大的狀況下性能並不明顯,將下降至少50%。
知足下面兩個條件咱們可使用SparseArray代替HashMap:
ArrayMap是一個
public class ArrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V> {}
複製代碼
構造函數由父類實現:
public ArrayMap() {
super();
}
public ArrayMap(int capacity) {
super(capacity);
}
public ArrayMap(SimpleArrayMap map) {
super(map);
}
複製代碼
HashMap內部有一個HashMapEntry[]對象,每個鍵值對都存儲在這個對象裏,當使用put方法添加鍵值對時,就會new一個HashMapEntry對象,而ArrayMap的存儲中沒有Entry這個東西,他是由兩個數組來維護的,mHashes數組中保存的是每一項的HashCode值,mArray中就是鍵值對,每兩個元素表明一個鍵值對,前面保存key,後面的保存value。
int[] mHashes;//key的hashcode值
Object[] mArray;//key value數組
複製代碼
SimpleArrayMap():建立一個空的ArrayMap,默認容量爲0,它會跟隨添加的item增長容量。 SimpleArrayMap(int capacity):指定特定容量ArrayMap; SimpleArrayMap(SimpleArrayMap map):指定特定的map;
public SimpleArrayMap() {
mHashes = ContainerHelpers.EMPTY_INTS;
mArray = ContainerHelpers.EMPTY_OBJECTS;
mSize = 0;
}
...
複製代碼
ArrayMap 的put(K key, V value):key 不爲null
/**
* Add a new value to the array map.
* @param key The key under which to store the value. <b>Must not be null.</b> If
* this key already exists in the array, its value will be replaced.
* @param value The value to store for the given key.
* @return Returns the old value that was stored for the given key, or null if there
* was no such key.
*/
public V put(K key, V value) {
final int hash;
int index;
//key 不能爲null
if (key == null) { //key == null,hash爲0
hash = 0;
index = indexOfNull();
} else {//獲取key的hash值
hash = key.hashCode();
index = indexOf(key, hash);//獲取位置
}
//返回index位置的old值
if (index >= 0) {
index = (index<<1) + 1;
final V old = (V)mArray[index];//old 賦值 value
mArray[index] = value;
return old;
}
//不然按位取反
index = ~index;
//擴容 System.arrayCopy數據
if (mSize >= mHashes.length) {
final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
: (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
final int[] ohashes = mHashes;
final Object[] oarray = mArray;
allocArrays(n);//申請數組
if (mHashes.length > 0) {
if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
System.arraycopy(oarray, 0, mArray, 0, oarray.length);
}
freeArrays(ohashes, oarray, mSize);//從新收縮數組,釋放空間
}
if (index < mSize) {
if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
+ " to " + (index+1));
System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
}
//最後 mHashs數組存儲key的hash值
mHashes[index] = hash;
mArray[index<<1] = key;//mArray數組相鄰位置存儲key 和value值
mArray[(index<<1)+1] = value;
mSize++;
return null;
}
複製代碼
從最後能夠看出:ArrayMap的存儲中沒有Entry這個東西,他是由兩個數組來維護的,mHashes數組中保存的是每一項的HashCode值,mArray中就是鍵值對,每兩個元素表明一個鍵值對,前面保存key,後面的保存value。
ArrayMap 的get(Object key):從Array數組得到value
/**
* Retrieve a value from the array.
* @param key The key of the value to retrieve.
* @return Returns the value associated with the given key,
* or null if there is no such key.
*/
public V get(Object key) {
final int index = indexOfKey(key);//得到key在Array的存儲位置
return index >= 0 ? (V)mArray[(index<<1)+1] : null;//若是index>=0 取(index+1)上的value值,不然返回null(從上面put知道array存儲是key(index) value(index+1)存儲的)
}
複製代碼
ArrayMap 和 HashMap區別:
HashMap內部有一個HashMapEntry[]對象,每個鍵值對都存儲在這個對象裏,當使用put方法添加鍵值對時,就會new一個HashMapEntry對象
ArrayMap的存儲中沒有Entry這個東西,他是由兩個數組來維護的
mHashes數組中保存的是每一項的HashCode值,
mArray中就是鍵值對,每兩個元素表明一個鍵值對,前面保存key,後面的保存value。
複製代碼
HashMap使用New的方式申請空間,並返回一個新的對象,開銷會比較大
ArrayMap用的是System.arrayCopy數據,因此效率相對要高。
複製代碼
三、ArrayMap提供了數組收縮的功能,只要判斷過判斷容量尺寸,例如clear,put,remove等方法,只要經過判斷size大小觸發到freeArrays或者allocArrays方法,會從新收縮數組,釋放空間。
四、ArrayMap相比傳統的HashMap速度要慢,由於查找方法是二分法,而且當你刪除或者添加數據時,會對空間從新調整,在使用大量數據時,效率低於50%。能夠說ArrayMap是犧牲了時間換區空間。但在寫手機app時,適時的使用ArrayMap,會給內存使用帶來可觀的提高。ArrayMap內部仍是按照正序排列的,這時由於ArrayMap在檢索數據的時候使用的是二分查找,因此每次插入新數據的時候ArrayMap都須要從新排序,逆序是最差狀況;
直接看:www.jianshu.com/p/7b9a1b386…測試對比
1.插入性能時間對比
數據量小的時候,差別並不大(固然了,數據量小,時間基準小,確實差別不大),當數據量大於5000左右,SparseArray,最快,HashMap最慢,乍一看,好像SparseArray是最快的,可是要注意,這是順序插入的。也就是SparseArray和Arraymap最理想的狀況。
倒序插入:數據量大的時候HashMap遠超Arraymap和SparseArray,也前面分析一致。 固然了,數據量小的時候,例如1000如下,這點時間差別也是能夠忽略的。
SparseArray在內存佔用方面的確要優於HashMap和ArrayMap很多,經過數據觀察,大體節省30%左右,而ArrayMap的表現正如前面說的,優化做用有限,幾乎和HashMap相同。
2.查找性能對比
1.在數據量小的時候通常認爲1000如下,當你的key爲int的時候,使用SparseArray確實是一個很不錯的選擇,內存大概能節省30%,相比用HashMap,由於它key值不須要裝箱,因此時間性能平均來看也優於HashMap,建議使用!
2.ArrayMap相對於SparseArray,特色就是key值類型不受限,任何狀況下均可以取代HashMap,可是經過研究和測試發現,ArrayMap的內存節省並不明顯,也就在10%左右,可是時間性能確是最差的,固然了,1000之內的若是key不是int 能夠選擇ArrayMap。