SparseArray与ArrayMap是Android提供的两个列表数据结构。SparseArray相比于HashMap采用的是,时间换取空间的方式来提高手机App的运行效率。而ArrayMap实现原理上也类似于SparseArray。
ArrayMap、SparseArray 和 HashMap 是三兄弟,但它们各有绝活,用在不同的场景。
一句话总结选择策略:
- 通用型,键是对象: 小数据用
ArrayMap,大数据用HashMap。 - 键是
int: 几乎总是用SparseArray或其变体。 - 键是
long: 用LongSparseArray。
一、核心区别总览
| 特性 | HashMap | ArrayMap | SparseArray |
|---|---|---|---|
| 键 (Key) 类型 | 任何 Object (e.g., String, CustomClass) |
任何 Object (e.g., String, CustomClass) |
只能是 int |
| 值 (Value) 类型 | 任何 Object | 任何 Object | 任何 Object |
| 内部结构 | 数组 + 链表/红黑树 | 两个平行数组 (int[], Object[]) |
两个平行数组 (int[], Object[]) |
| 内存开销 | 大 (每个元素是一个Node对象) | 小 (只有两个数组) | 极小 (没有自动装箱,只有两个数组) |
| 查找性能 | O(1) (平均,哈希直接定位) | O(log n) (二分查找哈希数组) | O(log n) (二分查找key数组) |
| 插入/删除性能 | O(1) (平均,哈希直接定位) | O(n) (可能需要移动数组元素) | O(n) (可能需要移动数组元素) |
| 迭代性能 | 慢 (需要遍历桶和链表/树) | 快 (顺序遍历数组,缓存友好) | 快 (顺序遍历数组,缓存友好) |
| 核心优势 | 查找极快,通用性强 | 内存效率高,适用于对象键的小数据集 | 内存效率极高 ,避免自动装箱 |
| 核心劣势 | 内存开销大 | 大数据量下性能下降明显 | 键只能是int,大数据量下性能下降 |
| 线程安全 | 否 (可用 ConcurrentHashMap) |
否 | 否 |
| 数据量建议 | 中到大数据 (数百以上) | 小到中数据 (千级以下) | 小到中数据 (千级以下) |
二、深入解析与选择策略
1. HashMap: 通用之王,性能至上
-
工作原理 :基于哈希表。通过
key.hashCode()计算数组索引,实现快速访问。处理冲突使用链表,过长时转为红黑树。 -
内存开销大的原因 :每个键值对都是一个
HashMap.Node对象(包含hash,key,value,next等字段),会产生大量小对象和开销。 -
选择时机:
- ✅ 当你的键不是基本类型 (例如
String,Uri, 自定义对象)。 - ✅ 当你要存储的数据量很大(例如超过 1000 个条目)。
- ✅ 当你需要极快的查找、插入、删除速度,并且内存不是首要考虑因素。
- ✅ 当你的键不是基本类型 (例如
2. ArrayMap: 内存优化的 HashMap 替代品
-
工作原理 :使用两个数组。一个
int[] mHashes存储所有键的哈希值,一个Object[] mArray交替存储键和值[key1, value1, key2, value2, ...]。通过对mHashes进行二分查找来定位元素。 -
内存开销小的原因:避免了为每个条目创建额外的 Node 对象,所有数据都紧凑地存储在数组中。
-
选择时机:
- ✅ 当你的键是对象 (如
String),但数据量不大(例如保存 Fragment 参数、Intent extras、配置项)。 - ✅ 当内存比绝对的查找速度更重要时。
- ✅ 当你需要频繁遍历 所有元素时(迭代性能比
HashMap好)。 - ⚠️ 注意 :
Bundle(Intent的数据载体)内部就使用ArrayMap,这已经为你做出了示范。
- ✅ 当你的键是对象 (如
3. SparseArray: 为 int 键而生的终极武器
-
工作原理 :与
ArrayMap极其相似,但专门为int键优化。它有一个int[] mKeys来存储键,一个Object[] mValues来存储值。直接对mKeys数组进行二分查找。 -
内存开销极小的原因:
- 避免自动装箱(Key Boxing) :这是它最大的优势。如果用
HashMap<Integer, Object>,每次插入和查找都会将int包装成一个Integer对象,产生额外开销。SparseArray的键是原生int数组,完全避免了这个问题。 - 同样没有额外的 Node 对象开销。
- 避免自动装箱(Key Boxing) :这是它最大的优势。如果用
-
选择时机:
- ✅ 只要你的键是
int类型 (例如viewId,resourceId, 数据库主键_id),就应优先考虑SparseArray。 - ✅ 适用于数据量不大的场景(千级以下)。
- ✅ 只要你的键是
SparseArray 家族变体:
SparseIntArray:Key为int,Value为int。用于替代HashMap<Integer, Integer>。SparseLongArray:Key为int,Value为long。LongSparseArray:Key为long,Value为Object。用于替代HashMap<Long, Object>。SparseBooleanArray:Key为int,Value为boolean。
三、实战代码示例与对比
假设有一个场景:用 View 的 ID (int) 作为键,存储某个自定义对象 ViewState。
方案 1: 使用 HashMap(不推荐)
scss
// 🚨 较差的选择:存在自动装箱开销
val viewStatesHashMap = HashMap<Int, ViewState>()
val viewId = R.id.my_button // 这是一个int
// 插入时:编译器会执行 Integer.valueOf(viewId),创建一个Integer对象
viewStatesHashMap[viewId] = ViewState()
// 查找时:同样会执行 Integer.valueOf(viewId),可能创建新的Integer对象
val state = viewStatesHashMap[viewId]
方案 2: 使用 SparseArray(推荐)
kotlin
// ✅ 最佳选择:避免自动装箱,内存效率高
val viewStatesSparseArray = SparseArray<ViewState>()
val viewId = R.id.my_button
// 插入和查找都直接使用原生int,无额外开销
viewStatesSparseArray.put(viewId, ViewState())
val state = viewStatesSparseArray.get(viewId)
// SparseArray 还可以通过key的索引直接操作,适合遍历
for (i in 0 until viewStatesSparseArray.size()) {
val key = viewStatesSparseArray.keyAt(i) // 直接拿到int类型的key
val value = viewStatesSparseArray.valueAt(i) // 直接拿到value
// ... 处理逻辑
}
另一个场景:键是 String(例如服务器返回的JSON数据)
arduino
// 数据量小(例如一个对象的几个字段)
val configData = ArrayMap<String, String>()
configData["theme"] = "dark"
configData["language"] = "en"
// 数据量大(例如一个长列表的数据)
val bigDataMap = HashMap<String, User>() // 更好的选择
// val bigDataMap = ArrayMap<String, User>() // 🚨 如果数据量大,性能会成为问题
四、最终选择决策树
当你需要选择一个结构时,可以遵循以下流程:
ini
graph TD
A[开始选择] --> B{键是什么类型?};
B --> C[键是 int 或 long];
C --> D{数据规模?};
D -- 小到中规模 --> E[✅ 首选 SparseArray<br>或 LongSparseArray];
D -- 大规模 --> F[✅ 考虑 HashMap];
B --> G[键是 String 或其他 Object];
G --> H{数据规模?};
H -- 小规模(千级以下) --> I[✅ 首选 ArrayMap];
H -- 中大规模 --> J[✅ 首选 HashMap];
subgraph Legend [图例说明]
K[小规模: 数十到数百条]
L[中规模: 数百到数千条]
M[大规模: 数千条以上]
end
总结黄金法则:
int键是SparseArray的天下,几乎总是首选。- 小的、对象键的集合(
Bundle, 参数, 配置)是ArrayMap的领域。 - 大的、需要极致性能的集合,或者是Java标准库代码,就用
HashMap。
遵循这些规则,应用将会更节省内存,在低端设备上表现更加流畅。