SparseArray 和 ArrayMap 都是 Android 特有的集合类,出现在 android.util
包下。
它们的诞生原因是 Android 在内存和性能优化上的特殊需求 ------ 在移动设备上,内存、GC 频率和性能都是敏感指标,所以 Google 给我们提供了比 Java 标准集合更轻量的替代方案。
它们在功能上与 Java 标准库中的 HashMap
类似,都是键值对(Key-Value)存储,但在内部实现上做了显著的改进。
一、SparseArray
SparseArray 是一种将整数映射到对象的键值对数据结构。它的设计初衷是为了替代 HashMap<Integer, Object>
,特别是在键是连续或稀疏的整数时。
它在系统源码和三方库中很常见,但因没有去了解过它,以至于很长时间都不知道它是干什么用的,只知道看上去和 HashMap 很像。
特点:两个数组(key 升序排列,value 对应存储)
vbnet
mKeys: [ 1, 3, 5 ]
mValues: ["A", "B", "C"]
查找流程:
1. 用二分查找 mKeys 找到 key 的位置
2. 用下标去 mValues 取值
结构示意:
rust
┌──────────────┐ ┌──────────────┐
│ 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>
SparseBooleanArray
Key = int,Value = boolean ,替代HashMap<Integer, Boolean>
SparseIntArray
Key = int,Value = int ,替代HashMap<Integer, Integer>
SparseLongArray
Key = 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)
vbnet
mHashes: [100, 200, 350]
mArray: [ "key1", "val1", "key2", "val2", "key3", "val3" ]
结构示意:
rust
┌────────────────┐ ┌────────────────────────────────┐
│ 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
查找下标的操作:
arduino
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 + 1
if (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 更省内存
- 查找是二分法 + 数组结构
- 插入删除需要移动数据,性能不适合超大数据集