Java填坑系列之SparseArray

前言
今天我們來了解一下與HashMap類似的資料結構SparseArray,並分析下它的原始碼實現。在分析原始碼的過程中,我們帶著以下幾個問題來看。
- SparseArray底層資料結構是什麼?
- SparseArray如何通過key獲得對應陣列下標
- SparseArray的擴容機制是什麼?
- SparseArray與HashMap有什麼區別?
核心欄位
private static final Object DELETED = new Object(); private boolean mGarbage = false; private int[] mKeys; private Object[] mValues; private int mSize; 複製程式碼
首先我們先來了解一下SparseArray類中宣告的變數都是做什麼的,如下
- DELETED 表示刪除狀態(後面詳細說明)
- mGarbage 表示是否GC
- mKeys 表示Key陣列,SparseArray中專門存取Key的陣列
- mValues 表示Values陣列,SparseArray中專門存取Value的陣列
- mSize 表示陣列實際儲存的元素大小
小結
通過了解以上幾個變數,我們可以大概知道SparseArray底層是通過兩個陣列來實現的,一個int陣列來存取Key,一個Object陣列來存取Value。
構造方法
public SparseArray() { this(10); } public SparseArray(int initialCapacity) { if (initialCapacity == 0) { mKeys = EmptyArray.INT; mValues = EmptyArray.OBJECT; } else { mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity); mKeys = new int[mValues.length]; } mSize = 0; } 複製程式碼
可以看出SparseArray預設容量為10,然後我們來看一下put()方法。
put()
public void put(int key, E value) { //通過key獲取對應的陣列位置 int i = ContainerHelpers.binarySearch(mKeys, mSize, key); //若i>=0,說明key存在,直接賦值 if (i >= 0) { mValues[i] = value; } else { //此時i<0,然後對i取反 i = ~i; //如果i<mSize並且i對應的Value已經是標記位刪除狀態,那麼就複用這個位置 if (i < mSize && mValues[i] == DELETED) { mKeys[i] = key; mValues[i] = value; return; } //如果需要GC並且mSize大於等於mKeys陣列的長度,那麼進行GC,並且重新查詢i if (mGarbage && mSize >= mKeys.length) { gc(); i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); } //最後分別插入key和value,並且判斷是否需要擴容 mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); mValues = GrowingArrayUtils.insert(mValues, mSize, i, value); mSize++; } } 複製程式碼
通過分析put()方法原始碼,我們可以知道SparseArray新增元素可以分為以下幾步。
- 獲取key對應的陣列位置i
- 判斷i是否大於0
- 如果i>0,說明key存在,直接賦值
- 如果i<0,說明key不存在
- 如果i<mSize並且i對應的Value已經是標記位刪除狀態,那麼就複用這個位置
- 如果需要GC並且mSize大於等於mKeys陣列的長度,那麼進行GC,並且重新查詢i
- 最後分別插入key和value,並且判斷是否需要擴容
查詢key對應的陣列下標
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; } } return ~lo; } 複製程式碼
可以看出通過二分查詢的方式來獲得key在陣列中對應的下標,最後如果沒找到,會對lo取反並返回。
GC相關方法
private void gc() { //表示實際大小 int n = mSize; int o = 0; int[] keys = mKeys; Object[] values = mValues; //遍歷所有元素,如果某個元素標記為DELETED,那麼就刪除 for (int i = 0; i < n; i++) { Object val = values[i]; if (val != DELETED) { if (i != o) { keys[o] = keys[i]; values[o] = val; values[i] = null; } o++; } } //設為false,表示不需要GC mGarbage = false; mSize = o; } 複製程式碼
擴容機制
在插入key和value時呼叫了GrowingArrayUtils的insert()方法,然後我們來看一下SparseArray裡如何進行擴容的,原始碼如下。
public static int[] insert(int[] array, int currentSize, int index, int element) { assert currentSize <= array.length; //不需要擴容 if (currentSize + 1 <= array.length) { //從index以後的元素向後移一位 System.arraycopy(array, index, array, index + 1, currentSize - index); //在index對應的位置賦值 array[index] = element; return array; } //需要擴容,建立一個新陣列 int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize)); //將array陣列中從0到index之間的元素(不包括index對應的元素)複製到新陣列中 System.arraycopy(array, 0, newArray, 0, index); newArray[index] = element; //再將index之後的元素複製到新陣列中 System.arraycopy(array, index, newArray, index + 1, array.length - index); return newArray; } public static int growSize(int currentSize) { //如果currentSize小於等於4,就為8,否則乘以2 return currentSize <= 4 ? 8 : currentSize * 2; } 複製程式碼
通過分析以上方法的原始碼,我們知道了SparseArray的擴容機制,主要步驟如下。
- 建立一個新陣列,陣列容量根據currentSize來判斷
- 將舊陣列中,index之前的陣列元素複製到新陣列中
- 對新陣列中的index對應的元素進行賦值
- 將舊陣列中,index之後的陣列元素複製到新陣列中
刪除操作
我們再來看一下SparseArray的刪除方法,通過檢視原始碼可以發現有多個刪除方法,我們一個個的來看一下。
remove(int key)
public void remove(int key) { delete(key); } public void delete(int key) { //通過key獲得對應的陣列下標i int i = ContainerHelpers.binarySearch(mKeys, mSize, key); if (i >= 0) { //如果mValues[i]沒有被標記為DELETED,那麼就進行標記,並設定mGarbage為true,表示需要GC if (mValues[i] != DELETED) { mValues[i] = DELETED; mGarbage = true; } } } 複製程式碼
可以看出該方法是通過key來進行刪除,主要分為以下幾步。
- 獲得key對應的陣列下標
- 如果i>=0,判斷mValues[i]是否被標記為DELETED
- 如果沒有被標記為DELETED,那麼就進行標記,並設定mGarbage為true,表示需要GC
removeAt(int index)
public void removeAt(int index) { if (mValues[index] != DELETED) { mValues[index] = DELETED; mGarbage = true; } } 複製程式碼
通過index找到對應的位置進行刪除操作。
查詢
get(int key)
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]; } } 複製程式碼
從以上原始碼可以看出SparseArray通過key來查詢對應的元素,主要有以下幾步。
- 獲取key對應的陣列下標
- 如果i小於0或者mValues[i]已經標記為DELETED,返回valueIfKeyNotFound,也就是null
- 否則就返回mValues[i]
SparseArray與HashMap區別
效能
在效能方面,SparseArray適合儲存少量的資料。如果儲存大量的資料,在進行擴容時,會有比較大的效能開銷。
底層資料結構
SparseArray是由兩個陣列組成,一個數組負責儲存key(int型別),一個數組負責儲存value,類似於key為int型別的HashMap。
刪除
SparseArray的刪除操作先將要刪除的陣列元素標記為DELETED,然後當儲存相同key對應的value時,可以進行復用。
總結
SparseArray在記憶體方面上比HashMap消耗更少,所以對於資料不大的情況下,優先使用SparseArray,畢竟在Android中記憶體是比較重要的。