SparseArray 和 ArrayMap 都是 Android 特有的集合类,出现在 android.util 包下。
它们的诞生原因是 Android 在内存和性能优化上的特殊需求 —— 在移动设备上,内存、GC 频率和性能都是敏感指标,所以 Google 给我们提供了比 Java 标准集合更轻量的替代方案。
它们在功能上与 Java 标准库中的 HashMap 类似,都是键值对(Key-Value)存储,但在内部实现上做了显著的改进。
一、SparseArray
SparseArray 是一种将整数映射到对象的键值对数据结构。它的设计初衷是为了替代 HashMap<Integer, Object>,特别是在键是连续或稀疏的整数时。
它在系统源码和三方库中很常见,但因没有去了解过它,以至于很长时间都不知道它是干什么用的,只知道看上去和 HashMap 很像。
特点:两个数组(key 升序排列,value 对应存储)
mKeys: [ 1, 3, 5 ]
mValues: ["A", "B", "C"]查找流程:
1. 用二分查找 mKeys 找到 key 的位置
2. 用下标去 mValues 取值结构示意:
┌──────────────┐ ┌──────────────┐
│ mKeys int[] │ │ mValues Obj[]│
├─────┬────────┤ ├─────┬────────┤
│ 0 │ 1 │ --> │ 0 │ "A" │
│ 1 │ 3 │ --> │ 1 │ "B" │
│ 2 │ 5 │ --> │ 2 │ "C" │
└─────┴────────┘ └─────┴────────┘删除元素时不会立刻移动数组,而是标记为“DELETED”占位符,等需要时再 gc() 进行压缩
二分查找大家都懂吧,就是那个过程很简单,细节是魔鬼的算法,简单介绍下,在给定一个有序的数据结构中去查找一个元素,通过目标元素的特征与中间的元素比较确定它属于哪半部分,往复循环,每次都是缩小目标数据结构的一半,直到找到元素,很好理解是不是,恩,保持你对它的第一印象就行了。
原理
SparseArray 内部使用两个并行的数组:一个用于存储键(int[] mKeys),另一个用于存储值(Object[] mValues)。当添加一个键值对时,它会根据键的大小将键插入到 mKeys 数组的正确位置,并同时将值插入到 mValues 数组的对应位置。由于数组是连续存储的,查找操作使用二分查找(binary search),其时间复杂度为 O(logn),而添加和删除操作可能需要移动数组元素,其时间复杂度为 O(n)。
作用与功能
SparseArray 避免了 HashMap 中每个键值对都需要创建一个 Entry 对象的开销。在 HashMap 中,每个 Entry 对象包含了键、值以及指向下一个 Entry 的引用,这些都会占用额外的内存。而 SparseArray 仅仅存储原始的 int 键和 Object 值,因此内存消耗更低。
Java 泛型不支持基本类型,Integer 会频繁装箱(int → Integer)和拆箱(Integer → int),增加 CPU 与内存占用。SparseArray就避免了频繁装箱/拆箱。
虽然添加和删除操作可能较慢,但对于查找操作,二分查找在数据量不大的情况下(通常在数百或数千个元素以内),其性能可以媲美甚至超过 HashMap。数据量大时,性能退化至少 50 %!
SparseArray 家族
SparseArray 只能存储 key 为 int 的数据,当我们要存储的数据 key 不为 int 类型或 value 是基本类型 怎么办?不慌,下面有请SparseArray 家族成员登场
SparseArray<E>Key = int,Value = Object,替代HashMap<Integer, E>SparseBooleanArrayKey = int,Value = boolean,替代HashMap<Integer, Boolean>SparseIntArrayKey = int,Value = int,替代HashMap<Integer, Integer>SparseLongArrayKey = int,Value = long,替代HashMap<Integer, Long>LongSparseArray<E>Key = long,Value = Object,替代HashMap<Long, E>
应用场景
- 键为
int类型:当你的键是整数时,这是首选。 - 数据量相对较小:如果键值对的数量不多,
SparseArray的性能优势更为明显。 - 键是稀疏的:即使键的数值范围很大,但实际使用的键数量不多,
SparseArray也能很好地工作。例如,你可能需要将视图 ID(如R.id.my_view)映射到某个对象,这些 ID 是不连续的,但总数不多。
在安卓中常见的就是存储监听/订阅列表或存储 View 之类的。
缺点
- 插入时会用二分查找 + 数组移动,数据量大时效率比 HashMap 低
- 不是线程安全的
- 删除操作延迟清理(gc() 机制)
如果只使用SparseArray 你就会发现,还是 HashMap 好用,因为SparseArray 没有像HashMap 那种以对象为 key 的存储方式,不要慌,接下来登场的是另一个安卓特有集合《ArrayMap》
二、ArrayMap
ArrayMap 是一种通用型的键值对数据结构,它的设计目的是为了在键值对数量较少时,替代 HashMap 以获得更高的内存效率和更好的性能。
特点:一个 int[] mHashes(存 hashCode,升序),一个 Object[] mArray(交错存储 key 和 value)
mHashes: [100, 200, 350]
mArray: [ "key1", "val1", "key2", "val2", "key3", "val3" ]结构示意:
┌────────────────┐ ┌────────────────────────────────┐
│ mHashes int[] │ │ mArray Object[] │
├─────┬──────────┤ ├─────┬─────────────┬─────┬──────┤
│ 0 │ 100 │ --> │ 0 │ "key1" │ 1 │"val1"│
│ 1 │ 200 │ --> │ 2 │ "key2" │ 3 │"val2"│
│ 2 │ 350 │ --> │ 4 │ "key3" │ 5 │"val3"│
└─────┴──────────┘ └─────┴─────────────┴─────┴──────┘查找流程:
- 在
mHashes用二分查找 hashCode - 在
mArray中取出对应的 key,调用equals()比较 - 如果 key 相等,返回对应的 value
查找下标的操作:
int indexOf(Object key, int hash) {// 当前数据的大小final int N = mSize;// Important fast case: if nothing is in here, nothing to look for.// 如果大小为0,说明没有数据存在,返回 -1 表示插入if (N == 0) {return ~0;}// 二分查找到下标int index = binarySearchHashes(mHashes, N, hash);// If the hash code wasn't found, then we have no entry for this key.// 返回要插入的下标位置if (index < 0) {return index;}// If the key at the returned index matches, that's what we want.// 如果返回索引处的键匹配,那就是我们想要的。index 是 0 的话,那么它的值就是1的位置。// index<<1 就等于 0 + 1if (key.equals(mArray[index<<1])) {return index;}// Search for a matching key after the index.// 遇到冲突,开放地址发解决冲突,从前往后找int end;for (end = index + 1; end < N && mHashes[end] == hash; end++) {if (key.equals(mArray[end << 1])) return end;}// Search for a matching key before the index.// 从后往前找for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {if (key.equals(mArray[i << 1])) return i;}// Key not found -- return negative value indicating where a// new entry for this key should go. We use the end of the// hash chain to reduce the number of array entries that will// need to be copied when inserting.// 未找到是取反表示需要插入的位置return ~end;
}原理
与 SparseArray 类似,ArrayMap 也是通过两个并行的数组来实现的:一个用于存储键的哈希码(int[] mHashes),另一个用于存储键值对(Object[] mArray)。mArray 数组中,键和值是交替存储的,即 mArray = {key1, value1, key2, value2, ...}。查找、添加和删除操作的实现也类似于 SparseArray,首先通过二分查找找到键的哈希码,然后检查键是否相等。
作用与功能
ArrayMap 同样避免了为每个键值对创建 Entry 对象的开销,从而大大减少了内存占用。对于少量键值对,ArrayMap 的内存消耗远低于 HashMap。
在数据量较小(通常在数百个元素以内)时,ArrayMap 的查找、添加和删除操作由于其精简的内部结构和对哈希冲突的优化处理,其性能可以优于 HashMap。它不需要处理复杂的链表或红黑树结构,代码实现更轻量。
应用场景
- 键值对数量较少:
ArrayMap的性能拐点通常在数百个元素左右,当数据量超过这个范围,HashMap的性能优势会逐渐显现。 - 需要通用键值对存储:与
SparseArray不同,ArrayMap的键可以是任意对象,而不仅仅是int类型。 - 性能和内存都是关键考虑因素:在内存受限的移动设备上,例如在一个
RecyclerView的Adapter中存储少量的数据,ArrayMap是一个很好的选择。
总的来说 ArrayMap 就是 替代 HashMap<K,V> 的轻量实现。
缺点
- 插入/删除需要移动数组,数据量大时性能差
- 不是线程安全的
- 数据量大时性能不如 HashMap
三、总结
- SparseArray 系列:Key 是原始数值类型(int、long),替代
HashMap<Integer, T>,避免装箱节省内存。 - ArrayMap:Key 可以是任意对象,替代
HashMap<K,V>,小数据量节省内存。 - 共同点:
- 数据量少时比 HashMap 更省内存
- 查找是二分法 + 数组结构
- 插入删除需要移动数据,性能不适合超大数据集