SparseArray是Android framework中提供的轻量级的键值对数据结构,我们知道空间和效率从来都是相悖的,SparseArray的实现正是以时间来换取空间效率,适合小规模数据的存储。
下面来了解下SparseArray的特点,使用,并分析部分源码。
SparseArray特点
SparseArray以键值对的形式保存数据,key是int类型,并且是唯一的不允许重复的key,而value可以是任何object。
SparseArray是轻量级的,使用2个数组分别保存key和value,并通过数组下标来对应关联。key数组是保持有序的。
优点
相比HashMap更加节省内存空间,数据存储只依赖key和value2个数组,数组空间是可复用的,数据的存储密度较高。
因为key数组是有序的,通过key获取value相对高效。
缺点:
key数组是保持有序的,在插入和查找时,通过二分法确定位置key的下标,比较耗时;
插入删除等操作可能会移动数组数据。
综合来说,SparseArray适用于小数据量的键值对场景。数据量达到几百时,效率优势和HashMap相比已不明显。
使用SparseArray
插入
- SparseArray初始化时需要指定存储数据的具体类型
- 使用
put
方法,插入key和对应的value
SparseArray<String> strArray = new SparseArray<>();
strArray.put(3, "DDDD");
strArray.put(1, "AAAA");
strArray.put(4, "CCCC");
strArray.put(2, "BBBB");
遍历
SparseArray没有实现Iterable,只能通过手动循环遍历:
for(int i = 0;i<strArray.size();i++){
String value = strArray.valueAt(i);
//do sth
}
插入的4条数据,打印是根据key值从小到大的顺序输出的,这也印证了在执行插入之后,key数组是保持有序的:
value at 0:AAAA
value at 1:BBBB
value at 2:DDDD
value at 3:CCCC
删除
删除元素的方法有2个:
1. removeAt
,删除指定下标的value。
2. delete
通过key删除对应的value。
从以上插入的4个数据中删除其中2个:
strArray.remove(3);//删除key为3的值,即DDDD
strArray.removeAt(0);//删除数组中的第一个值,即AAAA
结果只剩下BBBB和CCCC:
value at 0:BBBB
value at 1:CCCC
通过key获取元素
通过get
方法传入key来获取对应的value:
Log.i(TAG, "get 1:" +strArray.get(1));
Log.i(TAG, "get 4:" +strArray.get(4));
输出:
get 1:null //key=1的值为AAAA,已经被删除,因此返回null
get 4:CCCC
查找
指定key或者value来查找其下标值:
1. indexOfKey
二分法从mKeys数组中查找key的下标
2. indexOfValue
遍历查找mValues数组中value元素的下标。
Log.i(TAG, "index of key 4:" +strArray.indexOfKey(4));
Log.i(TAG, "index of value BBBB:" +strArray.indexOfValue("BBBB"));
输出:
key=4的值,数组下标为1,
BBBB在value数组中下标为0
index of key 4:1
index of value BBBB:0
源码分析
SparseArray所实现的Cloneable
接口,其实是一个空接口,并没有实现什么特性。
public class SparseArray<E> implements Cloneable
mKeys和mValues数组
key和Object之间通过数组下标来对应,整形数组mKeys
用于保存键,而mValues
保存Object实例。
private int[] mKeys; //键
private Object[] mValues; //值
这2个数组会自动扩容,但从数组移除某个对象后,不会进行减容,数组会重新组织将空位转移到数组末端,留作后面重用。
删除
因为删除的操作有点特殊,先分析这部分。
无论是removeAt
还是 delete
方法,都不会清除key数组中的值,只会将对应的value标记为DELETED
,并将mGarbage
值置成true,等待插入,查找下标,获取元素等操作时重新组织数组内容。
这相当于挖了个坑,后面还需要填平。
public void removeAt(int index) {
if (mValues[index] != DELETED) {
mValues[index] = DELETED;
mGarbage = true;
}
}
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
if (mValues[i] != DELETED) {
mValues[i] = DELETED;
mGarbage = true;
}
}
}
gc方法
这个gc方法并不是java的内存回收方法,它负责将被删除的“坑”填平,填平的方式是使用后面的元素覆盖这个坑,将空位移到数组后端。
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) {
if (i != o) {
keys[o] = keys[i];
values[o] = val;
values[i] = null;
}
o++;
}
}
mGarbage = false;
mSize = o;
}
插入操作
插入操作有可能使数组扩容。
public void put(int key, E value) {
//二分查找key在mKeys数组中的下标,如没有搜索到即返回低点index的取反值(负数)
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) { //找到对应的key,那么直接将value放入到mValues数组
mValues[i] = value;
} else { //key不存在
i = ~i; //将负数的index再取反,就是插入新元素的位置
if (i < mSize && mValues[i] == DELETED) { //如果该位置的value刚好被删除,直接替换这个value即可
mKeys[i] = key;
mValues[i] = value;
return;
}
if (mGarbage && mSize >= mKeys.length) {
gc(); //清理被删除的值,重新整理key和value数组
//gc方法的执行影响了index值,因此需要重新确定新元素插入的位置
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//插入新的key和value
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
SparseArray并没有多高深的设计和算法,Android程序中小数据量的场景可以考虑使用。
暂时分析到这里。