一、简介

SparseArrays map integers to Objects. Unlike a normal array of Objects, there can be gaps in the indices. It is intended to be more memory efficient than using a HashMap to map Integers to Objects, both because it avoids auto-boxing keys and its data structure doesn't rely on an extra entry object for each mapping.

SparseArray 与普通的数组不一样,它允许元素之间可以空缺(普通数组的元素必需是连续的),所以 SparseArray 类似于 Map。但是它的内存使用率比 HashMap 更高效:

SparseArray不用自动装箱,HashMap需要自动装箱(例如:int 类型要变成 Integer 类型才能做为 Key );

SparseArray不需要依赖额外的数据结构(HashMap将元素封装成 Map.Entry);

Note that this container keeps its mappings in an array data structure, using a binary search to find keys. The implementation is not intended to be appropriate for data structures that may contain large numbers of items. It is generally slower than a traditional HashMap, since lookups require a binary search and adds and removes require inserting and deleting entries in the array. For containers holding up to hundreds of items, he performance difference is not significant, less than 50%.

SparseArray通过『二分法』来查找 key,当 SparseArray 数据太大时(元素太多了),它的执行效率还不如 HashMap,因为它通过『二分法』来添加或删除元素时,存在数组元素的重新移动(只有在空间不足时才会这样,下段话会解释)。

To help with performance, the container includes an optimization when removing keys: instead of compacting its array immediately, it leaves the removed entry marked as deleted. The entry can then be re-used for the same key, or compacted later in a single garbage collection step of all removed entries. This garbage collection will need to be performed at any time the array needs to be grown or the the map size or entry values are retrieved.

SparseArray为提高性能在删除数据时进行了优化,不会立即压缩数组而是为需要删除的条目打上标记,在往后需要数组扩容或者数据检索时进行数据清除和数组压缩,这样可以减少数组操作的频率,同时可以复用key值。

二、源码分析

2.1、成员变量

public class SparseArray implements Cloneable {
// 删除元素时,用 DELETED 来代替要删除的元素(标记 key 对应的元素已删除)
private static final Object DELETED = new Object();
private boolean mGarbage = false;
private int[] mKeys; // 存储 key 的数组
private Object[] mValues; // 存储 对象 的数组
private int mSize; // 当前已存在元素的个数
}

2.2、二分查找

class ContainerHelpers {
// This is Arrays.binarySearch(), but doesn't do any argument validation.
// 从这里可以看出来(如果元素存在):
// 1. 第1个元素一定是在 array 的 mid 位置
// 2. 第2个元素一定是在 array 的 上/下半区的 mid 位置
// 3. 依此类推,元素的分布是每个给定 array & 指定 size 的 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
}
}

大家注意,没有找到时,是对 lo 的取反。

一般情况,如果没有找到,我们都会返回 -1,表示没有找到,而这里的取反(正数取反就是负数),同样表示没有找到,但是,如果我们再取反(反反得正),就可以在该位置添加元素,是不是很巧妙?

2.3、添加对象(put)

/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, E value) {
// 二分查找,找到 i 为正数,未找到 i 为取反后的负数
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
// 找到,即相同的key,那么值就覆盖
mValues[i] = value;
} else {
// 反反得正
i = ~i;
// 如果 i 在数组长度范围内,且该处的值被标记为 DELETED(已删除),则直接覆盖
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
// 数组空间不够,且存在有被标记 DELETED 的元素,则压缩空间
if (mGarbage && mSize >= mKeys.length) {
// 该方法就是真正的移除 DELETED 的元素,并移动元素
gc();
// gc 后需要重新计算新的位置(因为元素移动)
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
// 如果上面没有做 gc 操作,则表示空间真的全满了,需要动态增加数组大小
// 这里就涉及到数组的整体拷背了
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}

2.4、gc

private void gc() {
int n = mSize; // 原数组中的个数
int o = 0; // 压缩后元素的个数
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {
Object val = values[i];
if (val != DELETED) {
// 前面有元素被删除,因此 i 肯定不等于 o
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
}

2.5、GrowingArrayUtils.insert

public static 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));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}

三、总结

SparseArray对比HashMap:

优势:

避免了基本数据类型的装箱操作

不需要额外的结构体,单个元素的存储成本更低

数据量小的情况下,随机访问的效率更高

延迟删除

二分查找返回值的特殊处理

劣势:

插入和删除操作需要操作数组,效率较低

数据量巨大时,复制数组成本巨大

数据量巨大时,二分查找效率也会明显下降