前言
最近有小夥伴去面試了,在羣裏分享的面試題有一道是關於SparseArray
的,本來是不想看的o(╥﹏╥)o,沒想到是個面試題,那沒辦法只能看看了。
概述
本文還是跟前面分析HashMap
、LruChache
的方式一樣分別介紹構造方法、增、刪、改、查方法。
這裏先概括下SparseArray
的實現有個初步的認識。
- 作爲存儲鍵值對的容器跟
HashMap
是有很大的不同的,它是通過兩個大小相同的數組分別存儲鍵和值,並且鍵只能是int
類型的。 - 鍵和值插入的數組的位置是相同的,是通過二分查找法得出的插入位置。所以鍵的數組也是有序的。
- 刪除的時候並不是直接刪除,而是給value添加一個標記,當要插入位置value爲刪除標記的時候可以重用,直到合適的時候才調用自己實現的
gc()
方法回收垃圾,壓縮數組。
構造方法
private static final Object DELETED = new Object();//刪除元素用到的刪除標記
private boolean mGarbage = false;//是否需要調用gc()方法壓縮數組標記位
private int[] mKeys;//存儲key的數組
private Object[] mValues;//存儲值的數組
private int mSize;//當前鍵值對個數
public SparseArray() {
this(10);//默認容量10
}
public SparseArray(int initialCapacity) {//初始化key和value數組
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
構造方法比較簡單就是初始化鍵和值的數組,默認容量爲10。
刪
這裏先說刪因爲增的時候會用到刪除的標記判斷。
public void remove(int key) {
delete(key);
}
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//通過二分查找法找到key對應的index
if (i >= 0) {//i>=0代表存在要刪除的鍵值對
if (mValues[i] != DELETED) {//如果值不爲DELETED標記
mValues[i] = DELETED;//將值置爲DELETED標記
mGarbage = true;//並將回收標記置爲true 等待合適時機回收
}
}
}
//二分查找法
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
}
刪除就是將key通過二分查找法找到插入的下標,然後將對應位置的值置爲DELETED
刪除標記並且將mGarbage
回收標記位置爲true等待合適時間回收。
這裏重點需要注意的是這個二分查找法,如果在數組中找到key對應的位置則直接返回下標,否則返回~lo
由於lo
一定是正數取反則爲負數所以如果返回值爲負數則代表在數組中未找到key,並且lo
是數組中大於key的第一個位置在增加新鍵值對的時候會作爲插入位置使用。
增
public void put(int key, E value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//通過二分查找法尋找key的下標
if (i >= 0) {//有相同的key
mValues[i] = value;//直接覆蓋值
} else {//沒有找到相同的key
i = ~i;//用前面刪除方法說道的二分查找的lo作爲要插入的位置
if (i < mSize && mValues[i] == DELETED) {//如果要插入位置是刪除標記則直接重用
mKeys[i] = key;//覆蓋key
mValues[i] = value;//覆蓋value
return;
}
if (mGarbage && mSize >= mKeys.length) {//如果有垃圾需要回收並且元素數量>=數組長度則調用gc方法回收 第二個判斷條件是爲了不要頻繁的調用gc()優化性能因爲gc()方法會壓縮數組涉及到數組的移動
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);//回收後重新計算下標
}
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);//插入key(可能擴容)
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);//插入value(可能擴容)
mSize++;//鍵值對數+1
}
}
通過二分查找法找到下標,如果存在相同key的鍵值對則直接覆蓋值,如果不存在則看要插入位置值是否爲DELETED
如果是則直接覆蓋key和value,如果不是則根據mGarbage && mSize >= mKeys.length
條件判斷是否要調用gc()
回收,回收會可能會造成數組移動所以需要重新計算插入下標,然後插入新的鍵值對到鍵數組和值數組,並鍵值對數量+1。
private void gc() {
// Log.e("SparseArray", "gc start with " + mSize);
int n = mSize;//鍵值對數量
int o = 0;//上一個值不是DELETED的下標
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {
if (i != o) {//如果當前的i不等於o,則會將i後面所有元素往前移覆蓋之前刪除標記的數組 壓縮數組
keys[o] = keys[i];//覆蓋鍵
values[o] = val;//覆蓋值
values[i] = null;//將i指向的值置爲null
}
o++;
}
}
mGarbage = false;//清理垃圾標記位置爲false
mSize = o;//更新鍵值對數
// Log.e("SparseArray", "gc end with " + mSize);
}
回收值數組中的DELETED
標記的元素,具體實現是發現DELETED
標記後將後面的元素整體往前移然後將最後的值置爲null。
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {//不需要擴容
System.arraycopy(array, index, array, index + 1, currentSize - index);//將數組index和後面的元素往後移動一位
array[index] = element;//在index位置插入element
return array;
}
int[] newArray = new int[growSize(currentSize)];//需要擴容創建新數組
System.arraycopy(array, 0, newArray, 0, index);//將index前的元素複製到新數組
newArray[index] = element;//在index位置插入新元素
System.arraycopy(array, index, newArray, index + 1, array.length - index);//將index和後面的元素依次複製到新數組
return newArray;
}
public static int growSize(int currentSize) {//如果size小於4則擴容爲8,否則當前容量*2
return currentSize <= 4 ? 8 : currentSize * 2;
}
插入的話跟ArrayList
是差不多的,唯一的區別是擴容,如果當前size小於4則變爲8,其他情況直接size*2。
改
public void setValueAt(int index, E value) {//根據傳入index下標修改value
if (mGarbage) {//是否需要回收,因爲是根據index修改值所以需要排除DELETED標記元素的影響
gc();
}
mValues[index] = value;
}
比較簡單沒啥可說的,需要注意的是SparseArray
中凡是根據index操作的方法都會判斷是否需要gc()
一下,以排除DELETED
標記元素的干擾。
查
public E get(int key) {
return get(key, null);
}
@SuppressWarnings("unchecked")
public E get(int key, E valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0 || mValues[i] == DELETED) {//沒找到
return valueIfKeyNotFound;
} else {//找到了
return (E) mValues[i];
}
}
其他方法
這裏我們看下與index相關的方法,驗證下上面所說的
需要注意的是
SparseArray
中凡是根據index操作的方法都會判斷是否需要gc()
一下,以排除DELETED
標記元素的干擾。
public int keyAt(int index) {
if (mGarbage) {
gc();
}
return mKeys[index];
}
@SuppressWarnings("unchecked")
public E valueAt(int index) {
if (mGarbage) {
gc();
}
return (E) mValues[index];
}
public int indexOfKey(int key) {
if (mGarbage) {
gc();
}
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++) {
if (mValues[i] == value) {
return i;
}
}
return -1;
}
public int indexOfValueByValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++) {
if (value == null) {
if (mValues[i] == null) {
return i;
}
} else {
if (value.equals(mValues[i])) {
return i;
}
}
}
return -1;
}
可以看到無一例外都是判斷了是否要進行垃圾回收然後在進行其他操作避免DELETED
標記元素的干擾。
最後總結下
與HashMap
相比
優點:
- 鍵是
int[]
避免了裝箱拆箱的消耗。 - 不需要像
HashMap
每一個鍵值對創建一個Node
對象存儲,減少對象的創建。 - 擴容時只需要數組擴容不需要重建哈希表。
缺點:
- 插入的時候需要移動數組,刪除後觸發
gc()
也會移動數組進行壓縮,效率低。 - 增、刪、查都是通過二分查找法找到鍵對應的下標在進行操作,時間效率低。
適用場景:數據量不大,空間比時間重要,key爲int的情況。
對於我們客戶端來說一般頁面數據不會過千,那麼SparseArray相對於HashMap在查詢上不會有太大的區別,但是在內存上有很大的優勢,所以綜上所述一般情況下(數據量不過千)用SparseArray會好些。