想看我更多文章:【張旭童的部落格】blog.csdn.net/zxt0601
想來gayhub和我gaygayup:【mcxtzhang的Github主頁】github.com/mcxtzhang
1 概述
在前文中,我們已經聊過了HashMap
和LinkedHashMap
ArrayMap
.所以如果沒看過,可以先閱讀
面試必備:HashMap原始碼解析(JDK8) ,
面試必備:LinkedHashMap原始碼解析(JDK8 ,
面試必備:ArrayMap原始碼解析
今天依舊是看看android sdk的原始碼。
本文將從幾個常用方法下手,來閱讀SparseArray
的原始碼。
按照從構造方法->常用API(增、刪、改、查)的順序來閱讀原始碼,並會講解閱讀方法中涉及的一些變數的意義。瞭解SparseArray
的特點、適用場景。
如果本文中有不正確的結論、說法,請大家提出和我討論,共同進步,謝謝。
2 概要
概括的說,SparseArray<E>
是用於在Android平臺上替代HashMap
的資料結構,更具體的說,
是用於替代key
為int
型別,value
為Object
型別的HashMap
。
和ArrayMap
類似,它的實現相比於HashMap
更加節省空間,而且由於key指定為int
型別,也可以節省int
–Integer
的裝箱拆箱操作帶來的效能消耗。
它僅僅實現了implements Cloneable
介面,所以使用時不能用Map
作為宣告型別來使用。
它也是執行緒不安全的,允許value為null。
從原理上說,
它的內部實現也是基於兩個陣列。
一個int[]
陣列mKeys
,用於儲存每個item的key
,key
本身就是int
型別,所以可以理解hashCode
值就是key
的值.
一個Object[]
陣列mValues
,儲存value
。容量和key
陣列的一樣。
類似ArrayMap
,
它擴容的更合適,擴容時只需要陣列拷貝工作,不需要重建雜湊表。
同樣它不適合大容量的資料儲存。儲存大量資料時,它的效能將退化至少50%。
比傳統的HashMap
時間效率低。
因為其會對key從小到大排序,使用二分法查詢key對應在陣列中的下標。
在新增、刪除、查詢資料的時候都是先使用二分查詢法得到相應的index,然後通過index來進行新增、查詢、刪除等操作。
所以其是按照key
的大小排序儲存的。
另外,SparseArray
為了提升效能,在刪除操作時做了一些優化:
當刪除一個元素時,並不是立即從value
陣列中刪除它,並壓縮陣列,
而是將其在value
陣列中標記為已刪除。這樣當儲存相同的key
的value
時,可以重用這個空間。
如果該空間沒有被重用,隨後將在合適的時機裡執行gc(垃圾收集)操作,將陣列壓縮,以免浪費空間。
適用場景:
- 資料量不大(千以內)
- 空間比時間重要
- 需要使用
Map
,且key
為int
型別。
示例程式碼:
SparseArray<String> stringSparseArray = new SparseArray<>();
stringSparseArray.put(1,"a");
stringSparseArray.put(5,"e");
stringSparseArray.put(4,"d");
stringSparseArray.put(10,"h");
stringSparseArray.put(2,null);
Log.d(TAG, "onCreate() called with: stringSparseArray = [" + stringSparseArray + "]");複製程式碼
輸出:
//可以看出是按照key排序的
onCreate() called with: stringSparseArray = [{1=a, 2=null, 4=d, 5=e, 10=h}]複製程式碼
3 建構函式
//用於標記value陣列,作為已經刪除的標記
private static final Object DELETED = new Object();
//是否需要GC
private boolean mGarbage = false;
//儲存key 的陣列
private int[] mKeys;
//儲存value 的陣列
private Object[] mValues;
//集合大小
private int mSize;
//預設建構函式,初始化容量為10
public SparseArray() {
this(10);
}
//指定初始容量
public SparseArray(int initialCapacity) {
//初始容量為0的話,就賦值兩個輕量級的引用
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
//初始化對應長度的陣列
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
//集合大小為0
mSize = 0;
}複製程式碼
建構函式 無亮點,路過。
關注一下幾個變數:
- 底層資料結構為
int[]
和Object[]
型別陣列。 mGarbage
: 是否需要GCDELETED
: 用於標記value陣列,作為已經刪除的標記
4 增 、改
4.1 單個增、改:
public void put(int key, E value) {
//利用二分查詢,找到 待插入key 的 下標index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果返回的index是正數,說明之前這個key存在,直接覆蓋value即可
if (i >= 0) {
mValues[i] = value;
} else {
//若返回的index是負數,說明 key不存在.
//先對返回的i取反,得到應該插入的位置i
i = ~i;
//如果i沒有越界,且對應位置是已刪除的標記,則複用這個空間
if (i < mSize && mValues[i] == DELETED) {
//賦值後,返回
mKeys[i] = key;
mValues[i] = value;
return;
}
//如果需要GC,且需要擴容
if (mGarbage && mSize >= mKeys.length) {
//先觸發GC
gc();
//gc後,下標i可能發生變化,所以再次用二分查詢找到應該插入的位置i
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入key(可能需要擴容)
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
//插入value(可能需要擴容)
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
//集合大小遞增
mSize++;
}
}
//二分查詢 基礎知識不再詳解
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
}
}
//若沒找到,則lo是value應該插入的位置,是一個正數。對這個正數去反,返回負數回去
return ~lo; // value not present
}
//垃圾回收函式,壓縮陣列
private void gc() {
//儲存GC前的集合大小
int n = mSize;
//既是下標index,又是GC後的集合大小
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
//遍歷values集合,以下演算法 意義為 從values陣列中,刪除所有值為DELETED的元素
for (int i = 0; i < n; i++) {
Object val = values[i];
//如果當前value 沒有被標記為已刪除
if (val != DELETED) {
//壓縮keys、values陣列
if (i != o) {
keys[o] = keys[i];
values[o] = val;
//並將當前元素置空,防止記憶體洩漏
values[i] = null;
}
//遞增o
o++;
}
}
//修改 標識,不需要GC
mGarbage = false;
//更新集合大小
mSize = o;
}複製程式碼
GrowingArrayUtils.insert:
//
public static int[] insert(int[] array, int currentSize, int index, int element) {
//斷言 確認 當前集合長度 小於等於 array陣列長度
assert currentSize <= array.length;
//如果不需要擴容
if (currentSize + 1 <= array.length) {
//將array陣列內元素,從index開始 後移一位
System.arraycopy(array, index, array, index + 1, currentSize - index);
//在index處賦值
array[index] = element;
//返回
return array;
}
//需要擴容
//構建新的陣列
int[] newArray = new int[growSize(currentSize)];
//將原陣列中index之前的資料複製到新陣列中
System.arraycopy(array, 0, newArray, 0, index);
//在index處賦值
newArray[index] = element;
//將原陣列中index及其之後的資料賦值到新陣列中
System.arraycopy(array, index, newArray, index + 1, array.length - index);
//返回
return newArray;
}
//根據現在的size 返回合適的擴容後的容量
public static int growSize(int currentSize) {
//如果當前size 小於等於4,則返回8, 否則返回當前size的兩倍
return currentSize <= 4 ? 8 : currentSize * 2;
}複製程式碼
-
二分查詢,若未找到返回下標時,與JDK裡的實現不同,JDK是返回
return -(low + 1); // key not found.
,而這裡是對 低位去反 返回。
這樣在函式呼叫處,根據返回值的正負,可以判斷是否找到index。對負index取反,即可得到應該插入的位置。 -
擴容時,當前容量小於等於4,則擴容後容量為8.否則為當前容量的兩倍。和
ArrayList,ArrayMap
不同(擴容一半),和Vector
相同(擴容一倍)。 -
擴容操作依然是用陣列的複製、覆蓋完成。類似
ArrayList
.
5 刪
5.1 按照key刪除
//按照key刪除
public void remove(int key) {
delete(key);
}
public void delete(int key) {
//二分查詢得到要刪除的key所在index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//如果>=0,表示存在
if (i >= 0) {
//修改values陣列對應位置為已刪除的標誌DELETED
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
//並修改 mGarbage ,表示稍後需要GC
mGarbage = true;
}
}
}複製程式碼
5.2 按照index刪除
public void removeAt(int index) {
//根據index直接索引到對應位置 執行刪除操作
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}複製程式碼
5.3 批量刪除
public void removeAtRange(int index, int size) {
//越界修正
final int end = Math.min(mSize, index + size);
//for迴圈 執行單個刪除操作
for (int i = index; i < end; i++) {
removeAt(i);
}
}複製程式碼
6 查
6.1 按照key查詢
//按照key查詢,如果key不存在,返回null
public E get(int key) {
return get(key, null);
}
//按照key查詢,如果key不存在,返回valueIfKeyNotFound
public E get(int key, E valueIfKeyNotFound) {
//二分查詢到 key 所在的index
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
//不存在
if (i < 0 || mValues[i] == DELETED) {
return valueIfKeyNotFound;
} else {//存在
return (E) mValues[i];
}
}複製程式碼
6.2 按照下標查詢
public int keyAt(int index) {
//按照下標查詢時,需要考慮是否先GC
if (mGarbage) {
gc();
}
return mKeys[index];
}
public E valueAt(int index) {
//按照下標查詢時,需要考慮是否先GC
if (mGarbage) {
gc();
}
return (E) mValues[index];
}複製程式碼
6.3查詢下標:
public int indexOfKey(int key) {
//查詢下標時,也需要考慮是否先GC
if (mGarbage) {
gc();
}
//二分查詢返回 對應的下標 ,可能是負數
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
public int indexOfValue(E value) {
//查詢下標時,也需要考慮是否先GC
if (mGarbage) {
gc();
}
//不像key一樣使用的二分查詢。是直接線性遍歷去比較,而且不像其他集合類使用equals比較,這裡直接使用的 ==
//如果有多個key 對應同一個value,則這裡只會返回一個更靠前的index
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}複製程式碼
- 按照value查詢下標時,不像key一樣使用的二分查詢。是直接線性遍歷去比較,而且不像其他集合類使用
equals
比較,這裡直接使用的 == - 如果有多個key 對應同一個value,則這裡只會返回一個更靠前的index
總結
SparseArray
的原始碼相對來說比較簡單,經過之前幾個集合的原始碼洗禮,很輕鬆就可以掌握大體流程和關鍵思想:時間換空間。
Android sdk中,還提供了三個類似思想的集合:
SparseBooleanArray
,value
為boolean
SparseIntArray
,value
為int
SparseLongArray
,value
為long
他們和SparseArray
唯一的區別在於value
的型別,SparseArray
的value
可以是任意型別。而它們是三個常使用的拆箱後的基本型別。