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
。
遵循这些规则,应用将会更节省内存,在低端设备上表现更加流畅。