我们平时在Android平台上开发应用的时候经常会使用Java中的api去处理一些东西,但是由于Android手机的内存,cpu的处理能力等等原因,java上的api可能会处理同一个问题上可能会需要更多的内存空间去完成。于是Android上就推出了一些自己的api去优化这些问题,比如说本章的SparseArray。
官方文档是这么介绍的:
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.
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,
the performance difference is not significant, less than 50%.
其将其翻译过来大概意思就是:将整数映射到对象中,它的目的是提高记忆的效率,避免了key的自动装箱操作而且它的数据结构不依赖于额外的条目对象为每个映射。但是此容器在保留数组数据结构的时候,使用的是二分查找去查找key,这种结构并不适合含有大量数据的,我们在查找、添加、删除条目的时候都要比传统的HashMap的速度要慢。
通过上面的使用案例我们知道了其使用方法,下面我们来看看其内部的原理,首先我们需要创建一个SparseArray对象,同时我们也可以指定大小:
//用于表示删除的数据标识的
private static final Object DELETED = new Object();
//是否启动执行垃圾回收
private boolean mGarbage = false;
// 该数组用于存放 key
private int[] mKeys;
// 该数组用于存放 value
private Object[] mValues;
//表示当前数组实际的存储数据的大小
private int mSize;
//我们在创建爱你SparseArray对象的时候,默认大小是10。
public SparseArray() {
this(10);
}
public SparseArray(int initialCapacity) {
if (initialCapacity == 0) {
mKeys = EmptyArray.INT;
mValues = EmptyArray.OBJECT;
} else {
//创建一个大小initialCapacity的数组mValues以及mKeys数组分别用于存储value
mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
mKeys = new int[mValues.length];
}
mSize = 0;
}
从上面的代码中我们可以看出创建SparseArray对象的时候会指定一个默认的数组大小10,同时创建两个数组,一个数组用于存放key,一个用于存放value。当我们创建完对象之后就可以对集合进行操作了。
1. 添加数据
我们使用 put(int key, Object value)添加数据,或者是使用append(int key, Object value) 是来追加数据,首先我们来看看put() 函数:
public void put(int key, E value) {
//使用二分查找算法查找数组中指定长度位置的内数据。
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {//如果key数组中已经存在了该key的话,则直接更新value就行了。
mValues[i] = value;
} else {//表示该key之前并没有存在数组中。
i = ~i;//取反之后还原到之前的值,i取反之后的值要么是0,要么是size。
//这里表示i的位置小于mSize并且该位置的元素是已经被删除,则重新赋值该位置
if (i < mSize && mValues[i] == DELETED) {
mKeys[i] = key;
mValues[i] = value;
return;
}
//这里表示是否启动了垃圾回收,并且长度是否大于mKey数组的长度。将空余的数组空间回收
if (mGarbage && mSize >= mKeys.length) {
gc();
// Search again because indices may have changed.
i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
//这里主要做的操作是对数组进行扩容,已经对数组中的数据进行排序,
//数组的排序就意味着数组需要进行内容的移动
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
//同时也将value的值保存到指定的mValues数组中
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
/**
* 二分查找算法,它的要求则是希望查找的数组是升序分布的; 根据指定的value,array,array size查找指定的位置
* return 如果查找到了则返回该数在数组中的位置,否则返回一个负数
*/
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要么是0,要么是就是size,
* 因为如果要查找的值大于最大值,或者是要查找的值小于最小值,所以
* 取反之后要么是-1, 要么就是一个更小的负数,所以如果没有查找到一定返回一个负数
*/
return ~lo;
}
从SDK5.0以后系统将元素插入到数组的一系列操作方法封装到com.android.internal.util.GrowingArrayUtils类里面了。下面是对数组中进行插入元素的方法;
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);
array[index] = element;
return array;
}
//如果实际的内容长度大于我们指定数组长度,这个时候需要对我们的数组进行扩容。
int[] newArray = ArrayUtils.newUnpaddedIntArray(growSize(currentSize));
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
2. 根据指定的key获取元素
当我们添加完数据之后,可以使用 get(int key)方法根据指定的key来获取对应的value。其实获取value是非常的简单的,因为我们在添加数据的同时也对数组进行升序排序了,就是直接对mKeys数组进行一个二分查找,找到对应的pos,因为mValue的存储跟mKeys的pos是一样的,所以就通过数组索引的方式获取到对应的value。
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];
}
}
3. 根据指定的key删除对应的value
使用delete(int key)或者是remove(int key) 根据指定的key来删除容器中对应的内容。根据源代码我们可以很清晰的看到删除元素其实就是根据二分查找算法找到对应key的pos,然后在pos上的value的设置成DELETED,同时将启动垃圾回收标识
public void delete(int key) {
//对mKeys进行二分查找,找到对应的key的位置
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {//如果数组mKeys中存在对应的key
if (mValues[i] != DELETED) {//则对应的mValues[i]标识为DELETED元素
mValues[i] = DELETED;
mGarbage = true;//同时将启动垃圾回收置 true。
}
}
}
public void remove(int key) {
delete(key);
}
我们之前在看put()数据的时候看到有一个垃圾回收的判断和一个元素是否等于DELETED的判断,如果存在就启动gc()。
private void gc() {
int n = mSize;
int o = 0;
int[] keys = mKeys;
Object[] values = mValues;
for (int i = 0; i < n; i++) {//遍历实际存储mSize的mValues数组。
Object val = values[i];
if (val != DELETED) {//这里判断val是否被标识成了DELETED
if (i != o) {
keys[o] = keys[i];
values[o] = val;
//将values[i]标识成null,等待系统垃圾回收期来回收。
values[i] = null;
}
o++;
}
}
//同时修改垃圾回收标识为false
mGarbage = false;
mSize = o;
}
4. 位置索引
使用indexOfKey(int key)根据key来获取改key在key数组中的位置,使用indexOfValue(E value)根据指定的value来获取该位置。
/**
* @return 大于或者是等于0表示存在,反之该值不存在数组中
*/
public int indexOfKey(int key) {
if (mGarbage) {//如果我们之前将垃圾回收的标志位设置了true,则先执行垃圾回收
gc();
}
/**
* 然后利用二分查找的算法找到对应的key在数组中的位置,之前我们在看二分查找算法的时候,
* 如果没有查找成功的话,则会返回一个小于0的值,如果查找到了则返回一个大于0的值
*
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
我们还可以根据value来获取该value在数组中的位置,其实只是遍历了数组,依次去查找而已的,如果不存在的话则返回-1,如果垃圾回收标识true的话先执行垃圾回收的操作。
public int indexOfValue(E value) {
if (mGarbage) {
gc();
}
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
从我们对源代码来看其原理就是对数组的增、删、改、查。内部维持着两个数组一个用于存储key,一个用处value,当我们向容器中添加数据的时候,同时也会对key数组进行一个排序插入始终保证数组的顺序都是升序的排列的。在根据key进行对应的查找的时候使用的是二分查找算法进行查找。该类的数据结构比较简单只是仅仅简单的数组结构,由此比较省内存,不用像HashMap那样子进行数据装箱的操作,所以该类在手机端的一些场景要比HashMap比较好,但是我们在插入数据的时候也涉及到排序的操作,如果当数据量比较大的时候可能会出现效率低的问题,不适合做大量数据的操作;其二就是该类的key只能是int型的在有些地方限制了其使用的,操作起来有时候并没有这么方便的。
其实当我们看了该类的源代码之后发现其内部也没有我们所说的这么高大上,其实也都是调用了一些基本的api,然后数据结构也是非常简单的,其实这个里面我觉得有一个最关键的东西就是当我们添加的数据大于数组的大小的时候,我们平时应该怎么操作呢?这里主要是用了System.arraycopy()数组的拷贝,还有就是我们大学里面一个最平常的算法二分查找算法,下一次我们将重点介绍数组拷贝背后的原理,因为System.arraycopy()
在很多的java api中都是有涉及到的,我们在对数组进行拷贝的时候同时也要考虑到数组的扩容,扩容的话就必须涉及到必须的申请更多的内容,涉及到内存分配了。