ArrayMap、SparseArray和HashMap有什么区别?该如何选择?

328 阅读5分钟

SparseArrayArrayMapAndroid提供的两个列表数据结构。SparseArray相比于HashMap采用的是,时间换取空间的方式来提高手机App的运行效率。而ArrayMap实现原理上也类似于SparseArray

ArrayMapSparseArray 和 HashMap 是三兄弟,但它们各有绝活,用在不同的场景。

一句话总结选择策略:

  • 通用型,键是对象: 小数据用 ArrayMap,大数据用 HashMap
  • 键是 int 几乎总是用 SparseArray 或其变体。
  • 键是 long 用 LongSparseArray

一、核心区别总览

特性HashMapArrayMapSparseArray
键 (Key) 类型任何 Object (e.g., StringCustomClass)任何 Object (e.g., StringCustomClass)只能是 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 对象(包含 hashkeyvaluenext 等字段),会产生大量小对象和开销。

  • 选择时机

    • ✅ 当你的键不是基本类型(例如 StringUri, 自定义对象)。
    • ✅ 当你要存储的数据量很大(例如超过 1000 个条目)。
    • ✅ 当你需要极快的查找、插入、删除速度,并且内存不是首要考虑因素。

2. ArrayMap: 内存优化的 HashMap 替代品

  • 工作原理:使用两个数组。一个 int[] mHashes 存储所有键的哈希值,一个 Object[] mArray 交替存储键和值 [key1, value1, key2, value2, ...]。通过对 mHashes 进行二分查找来定位元素。

  • 内存开销小的原因:避免了为每个条目创建额外的 Node 对象,所有数据都紧凑地存储在数组中。

  • 选择时机

    • ✅ 当你的键是对象(如 String),但数据量不大(例如保存 Fragment 参数、Intent extras、配置项)。
    • ✅ 当内存比绝对的查找速度更重要时。
    • ✅ 当你需要频繁遍历所有元素时(迭代性能比 HashMap 好)。
    • ⚠️ 注意BundleIntent 的数据载体)内部就使用 ArrayMap,这已经为你做出了示范。

3. SparseArray: 为 int 键而生的终极武器

  • 工作原理:与 ArrayMap 极其相似,但专门为 int 键优化。它有一个 int[] mKeys 来存储键,一个 Object[] mValues 来存储值。直接对 mKeys 数组进行二分查找。

  • 内存开销极小的原因

    1. 避免自动装箱(Key Boxing) :这是它最大的优势。如果用 HashMap<Integer, Object>,每次插入和查找都会将 int 包装成一个 Integer 对象,产生额外开销。SparseArray 的键是原生 int 数组,完全避免了这个问题。
    2. 同样没有额外的 Node 对象开销。
  • 选择时机

    • ✅ 只要你的键是 int 类型(例如 viewIdresourceId, 数据库主键 _id),就应优先考虑 SparseArray
    • ✅ 适用于数据量不大的场景(千级以下)。

SparseArray 家族变体:

  • SparseIntArrayKey 为 intValue 为 int。用于替代 HashMap<Integer, Integer>
  • SparseLongArrayKey 为 intValue 为 long
  • LongSparseArrayKey 为 longValue 为 Object。用于替代 HashMap<Long, Object>
  • SparseBooleanArrayKey 为 intValue 为 boolean

三、实战代码示例与对比

假设有一个场景:用 View 的 ID (int) 作为键,存储某个自定义对象 ViewState

方案 1: 使用 HashMap(不推荐)

// 🚨 较差的选择:存在自动装箱开销
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(推荐)

// ✅ 最佳选择:避免自动装箱,内存效率高
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数据)

// 数据量小(例如一个对象的几个字段)
val configData = ArrayMap<String, String>()
configData["theme"] = "dark"
configData["language"] = "en"

// 数据量大(例如一个长列表的数据)
val bigDataMap = HashMap<String, User>() // 更好的选择
// val bigDataMap = ArrayMap<String, User>() // 🚨 如果数据量大,性能会成为问题

四、最终选择决策树

当你需要选择一个结构时,可以遵循以下流程:

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

总结黄金法则:

  1. int 键是 SparseArray 的天下,几乎总是首选。
  2. 小的、对象键的集合(Bundle, 参数, 配置)是 ArrayMap 的领域。
  3. 大的、需要极致性能的集合,或者是Java标准库代码,就用 HashMap

遵循这些规则,应用将会更节省内存,在低端设备上表现更加流畅。