Android提供了SparseArray,这也是一种KV形式的数据结构,提供了相似于Map的功能。可是实现方法却和HashMap不同。它与Map相比,能够说是各有千秋。java
优势android
缺点git
相关参考 SparseArray vs HashMapgithub
总的来讲,SparseArray适用于数据量不是很大,同时Key又是数字类型的场景。
好比,存储某月中天天的某种数据,最多也只有31个,同时它的key也是数字(可使用1-31,也可使用时间戳)。
再好比,你想要存储userid与用户数据的映射,就可使用这个来存储。数组
接下来,我将讲解它的特性与实现细节。数据结构
它使用的是两个数组来存储数据,一个数组存储key,另外一个数组来存储value。随着咱们不断的增长删除数据,它在内存中是怎么样的呢?咱们须要有一个直观的认识,能帮助咱们更好的理解和体会它。函数
内部有两个数组变量来存储对应的数据,mKeys
用来存储key,mValues
用来存储泛型数据,注意,这里使用了Object[]
来存储泛型数据,而不是T[]
。为何呢?这个后面在讲。工具
以下图所示,插入数据,老是“紧贴”数组的左侧,换句话说,老是从最左边的一个空位开始使用。我一开始没详细探究的时候,都觉得它是相似HashMap
那样稀疏的存储。学习
另外一个值得注意的事情是,key老是有序的,无论通过多少次插入,key数组中,key老是从小到大排列。google
当一直插入数据,快满的时候,就会自动的扩容,建立一个更大的数组出来,将现有的数据所有复制过去,再插入新的数据。这是基于数组实现的数据结构共同的特性。
删除是使用标记删除的方法,直接将目标位置的有效元素设置为一个DELETED标记对象。
怎么查数据呢?
好比咱们查5这个数据get(5)
,那么它是在mKeys
中去查找是否存在5,若是存在,返回index,而后用这个index在对应的mValues
取出对应的值就行了。
接下来咱们按照本身的理解,来实现这样的一个数据结构,从而学习它的一些细节和思想,加深对它的理解,有利于在生产中,能更有效的,正确的使用它。
首先,肯定一下,咱们须要暴露什么样的功能给别人使用。固然了,答案是显而易见的,固然是插入,查询,删除等功能了。
public class SparseArray<E> { public SparseArray() { } public SparseArray(int initCap) { } public void put(int key, E value) { } public E get(int key) { } public void delete(int key) { } public int size() { } }
上面列举了咱们须要的功能,无参构造函数,有参数构造函数(指望能主动设置初始容量),put数据,get数据,删除数据,以及获取当前数据有多少。
put数据是最核心的方法,通常咱们开发一个东西,也是先开发建立数据的功能,这样才能接着开发展现数据的功能。因此咱们先来实现put方法。
按照以前的理解,咱们须要一些成员变量来存储数据。
private int[] mKeys; private Object[] mValues; private int mSize = 0;
须要先找到put到什么位置
这里会有两种状况:
所以第一步,须要先找一下,当前key,是否存在。咱们使用二分查找来处理。
public void put(int key, E value) { int i = BinarySearch.search(mKeys, mSize, key); if (i >= 0) { // 找到了有两种状况 // 1.是对应的mValues有一个有效的数据对象,直接覆盖 // 2.对应的mValues里面是一个DELETED对象,一样的,直接覆盖 mValues[i] = value; } else { } }
若是在数组中找到了,那么操做就很简单,直接覆盖就完事了。
若是没找到呢,咱们须要将数据插入到正确的位置上,这个所谓正确的位置,指的是,插入以后,依然保证数组有序的状况。打个比方:1, 4, 5, 8
,请问3
应该插入哪里,固然是放到index=1
的地方,结果就是1, 3, 4, 5, 8
了。
那若是key不存在,怎么知道应该放到哪里呢?
咱们来看一下这个二分查找,它帮咱们解决了这个小问题。
public static int search(int[] arr, int size, int target) { int lo = 0; int hi = size - 1; while (lo <= hi) { final int mid = (lo + hi) >>> 1; final int value = arr[mid]; if (value == target) { return mid; } else if (value > target) { hi = mid - 1; } else { lo = mid + 1; } } return ~lo; }
按照传统的思想,查找类的API,若是找不到,通常都会返回-1,可是这个二分查找,返回了lo的取反。这会达到什么效果呢。
状况1:数组是空的,那么查找任何东西,都找不到,那会怎么样?根据代码能够知道,循环都进不去,那么直接返回了~0
,也就是最大的负数。咱们只须要知道它是一个负数。
状况2:数组不是空的,好比1, 3, 5
,咱们找2
,这里简单的单步执行一下:
lo = 0, size = 3, hi = 2, 好,进入循环 mid = (0 + 2) / 2 = 1, value = 3 value > 2, 因此 hi = 1 - 1 = 0, 再次循环 mid = (0 + 0) / 2 = 0, value = 1 value < 2, so, lo = 0 + 1; 退出循环 返回~1
若是你在尝试去验算其余状况,你会发现,返回值恰好是它应该放置的位置的取反。换句话说,返回值再取反后,就能够获得,这个key应该插入的位置。
这应该是二分查找的一个小技巧。很是的实用!
接下来,想想,0取反是负数,任何正数取反,也都是负数,也就是说,只要是负数,就表明没找到,再将这个数取反,就获得了,应该put的位置!
因此,代码继续实现为:
public void put(int key, E value) { int i = BinarySearch.search(mKeys, mSize, key); if (i >= 0) { // 找到了有两种状况 // 1.是对应的mValues有一个有效的数据对象,直接覆盖 // 2.对应的mValues里面是一个DELETED对象,一样的,直接覆盖 mValues[i] = value; } else { i = ~i; mKeys = GrowingArrayUtil.insert(mKeys, mSize, i, key); mValues = GrowingArrayUtil.insert(mValues, mSize, i, value); mSize++; } }
接下来,咱们实现get方法。
get方法实现就比较简单了,只须要经过二分查找找到对应的index,再从value数组中取出对象便可。
public E get(int key) { // 首先查找这个key存不存在 int i = BinarySearch.search(mKeys, mSize, key); if (i < 0) { return null; } else { return (E)mValues[i]; } }
delete方法,就是删除某个key,对应的细节是,找到这个key是否存在,若是存在的话,将value数组中对应位置的数据设置为一个常量DELETED
。这样作的好处就是比较快捷,而不须要真正的去删除元素。固然因为这个DELETED对象存在value数组中,对put和get以及size方法都会带来一些影响。
下面的代码,定义一个静态的final变量DELETED
用来做为标记已经删除的变量。
另外一个成员变量标记,当前value数组中是否有删除元素这个状态信息。
private static final Object DELETED = new Object(); /** * 标记是否有DELETED元素标记 * */ private boolean mHasDELETED = false; public void delete(int key) { // 删除的时候为标记删除,先要找到是否有这个key,若是没有,就不必删除了; // 找到了key看一下对应的value是否已是DELETED,若是是的话,也不必再删除了 int i = BinarySearch.search(mKeys, mSize, key); if (i >= 0 && mValues[i] != DELETED) { mValues[i] = DELETED; mHasDELETED = true; } }
size方法返回在这个容器中,数据对象有多少个。因为DELETED
对象的存在,key数组和value数组,以及成员变量mSize
都无法靠谱得直接获得有效数据的count。
所以这里须要一个内部的工具方法gc()
,它的做用就是,若是有DELETED
对象存在,那么就从新整理一下数组,将DELETED
对象都移除,数组中只保留有效数据便可。
先来看gc
的实现
private void gc() { int placeHere = 0; for (int i = 0; i < mSize; i++) { Object obj = mValues[i]; if (obj != DELETED) { if (i != placeHere) { mKeys[placeHere] = mKeys[i]; mValues[placeHere] = obj; mValues[i] = null; } placeHere++; } } mHasDELETED = false; mSize = placeHere; }
它的内部逻辑很简单,就是从头至尾遍历value数组,把每个不是DELETED
的对象都从新放置一遍,覆盖掉前面的DELETED
对象。
而后,咱们再看一下size的实现
public int size() { if (mHasDELETED) { gc(); } return mSize; }
假设有这样的一个场景,put(1, a), put(2, b), delete(2), get(2)
。按照如今的get实现,就会返回DELETED
对象出去,因此,因为DELETED
的存在,咱们须要完善一下get方法的逻辑。
public E get(int key) { // 首先查找这个key存不存在 int i = BinarySearch.search(mKeys, mSize, key); // 这里有两种状况 // 若是key小于0,说明在mKeys中,没有目标key,没找到 // 若是key大于0,还要看一下,对应的mValues中,是否那个元素是DELETED,由于删除的时候是标记删除的 // 以上两种状况都是没有找到 if (i < 0 || mValues[i] == DELETED) { return null; } else { return (E)mValues[i]; } }
补充的代码上面我都写了注释,讲解了这两坨额外的代码是用来处理什么状况的。
public void put(int key, E value) { int i = BinarySearch.search(mKeys, mSize, key); if (i >= 0) { // 找到了有两种状况 // 1.是对应的mValues有一个有效的数据对象,直接覆盖 // 2.对应的mValues里面是一个DELETED对象,一样的,直接覆盖 mValues[i] = value; } else { i = ~i; // 这一段代码是处理这一的场景的 // 1 2 3 5, delete 5, put 4 if (i < mSize && mValues[i] == DELETED) { mKeys[i] = key; mValues[i] = value; return; } // 另外一种状况 // 若是有删除的元素,而且数组装满了,这个时候须要先GC,再从新搜一下key的位置 if (mHasDELETED && mSize >= mKeys.length) { gc(); i = ~BinarySearch.search(mKeys, mSize, key); } mKeys = GrowingArrayUtil.insert(mKeys, mSize, i, key); mValues = GrowingArrayUtil.insert(mValues, mSize, i, value); mSize++; } }
其实提及来很简单,用一个过程来归纳一下通常状况。
[1, 2, 3, 4, 5, 0, 0, 0, 0, 0] insert(index=2, value=99) 1.复制index=2之前的元素 [1, 2, 3, 4, 5, 0, 0, 0, 0, 0] 2.复制index=2之后的元素,日后挪一位 [1, 2, 3, 3, 4, 5, 0, 0, 0, 0] 3.将index=2的位置,放入99 [1, 2, 99, 3, 4, 5, 0, 0, 0, 0]
固然,这里要处理,若是恰好数据满了,插入新数据,就须要建立一个新的,更大的数组来复制之前的数据了。
/** * @param rawArr 原始数组 * @param size 有效数据的长度,与数组长度不同,若是数组长度大于有效数据的长度,那么往里面插入数据是OK的 * 若是有效数据的长度等于数组的长度,那么要插入数据,就要建立更大的数组 * @param insertIndex 插入index * @param insertValue 插入到index的数值 * */ public static int[] insert(int[] rawArr, int size, int insertIndex, int insertValue) { if (size < rawArr.length) { System.arraycopy(rawArr, insertIndex, rawArr, insertIndex + 1, size - insertIndex); rawArr[insertIndex] = insertValue; return rawArr; } int[] newArr = new int[rawArr.length * 2]; System.arraycopy(rawArr, 0, newArr, 0, insertIndex); newArr[insertIndex] = insertValue; System.arraycopy(rawArr, insertIndex, newArr, insertIndex + 1, size - insertIndex); return newArr; } public static <T> Object[] insert(Object[] rawArr, int size, int insertIndex, T insertValue) { if (size < rawArr.length) { System.arraycopy(rawArr, insertIndex, rawArr, insertIndex + 1, size - insertIndex); rawArr[insertIndex] = insertValue; return rawArr; } Object[] newArr = new Object[rawArr.length * 2]; System.arraycopy(rawArr, 0, newArr, 0, insertIndex); newArr[insertIndex] = insertValue; System.arraycopy(rawArr, insertIndex, newArr, insertIndex + 1, size - insertIndex); return newArr; }
好了,关于SparseArray
的讲解就到这里结束了。完整的源码能够查看我写的,也能够查看官方的。
Object[]
,而不是T[]
个人理解是,若是使用泛型数组T[]
,你就必须构造出一个泛型数组,那么构造泛型数组,你须要能建立泛型对象,也就是说,必须调用T
的构造函数才能建立泛型对象,可是因为是泛型,构造函数是不肯定的,只能经过反射的形式来调用,这样显然就效率和稳定性上有一些问题。所以大多数泛型的实现,都是经过Object
对象来存储泛型数据。
若是你以为这篇内容对你有帮助的话,不妨打赏一下,哪怕是小小的一份支持与鼓励,也会给我带来巨大的动力,谢谢:)