详解:ArrayMap和SparseArray在HashMap上面的改进

88 阅读4分钟

ArrayMapSparseArray 是 Android 中为优化内存和性能设计的两种数据结构,主要针对 HashMap 在某些场景下的不足进行改进。

一、HashMap的痛点

  1. 内存开销大:每个键值对需封装为 Entry 对象(含哈希值、键、值、链表指针),产生对象头、字段对齐等额外内存占用,小数据量时浪费显著。
  2. 自动装箱问题:若键/值为基本类型(如 int),需装箱为 Integer 等对象,引发内存碎片和频繁 GC(尤其在 Android 上影响流畅性)。
  3. 哈希冲突成本高:冲突时链表转为红黑树(JDK8+),增删操作复杂度波动;小数据量下哈希表空间利用率低。
  4. 扩容开销大:数据增长时需重建哈希表(resize),触发全量 rehash,临时内存翻倍且耗时。
  5. 小数据性能劣势:在数百元素内,哈希计算的常数开销可能高于 ArrayMap/SparseArray 的二分查找。

在 Android 小数据、低内存场景下,HashMap 易引发内存冗余和性能抖动,此时优先选用 ArrayMap 或 SparseArray,以下是它们的核心改进点及适用场景:

二、 ArrayMapHashMap 的改进

改进点:

  • 内存优化

    • 双数组结构:使用两个数组(mHashes 存储键的哈希值,mArray 交替存储键和值),避免了 HashMap 中 Entry 对象(包含 keyvaluenext 等字段)的内存开销。
    • 避免小对象分配:直接操作数组,减少了频繁创建 Entry 对象的内存碎片。
    • 扩容机制:扩容时按需增长(容量翻倍而非 HashMap 的 2 倍 + 1),减少内存浪费。
  • 性能优化(小数据量场景)

    • 二分查找:通过哈希值的二分查找确定键的位置,时间复杂度为 O(log n),在数据量小(如数百个元素)时性能接近 HashMap 的 O(1)
    • 缓存复用:全局缓存小容量的 ArrayMap 实例,减少内存分配。

适用场景:

  • 键值对数量较少(推荐 < 1000)。
  • 需要频繁创建和销毁 Map 对象(如 Android 的 Bundle 底层使用 ArrayMap)。
  • Key 为非原始类型(如 String 或 Object)。

缺点:

  • 数据量大时,查找性能劣于 HashMapO(log n) vs O(1))。
  • 插入/删除需移动数组元素,频繁操作效率较低。

三、SparseArray 对 HashMap 的改进

改进点:

  • 避免自动装箱

    • 专为 int 类型的 Key 设计(如 SparseArray<Value>),无需将 int 装箱为 Integer,减少内存开销和 GC 压力。
    • 类似变种:LongSparseArray(Key 为 long)、SparseBooleanArray 等。
  • 内存优化

    • 使用两个平行数组:mKeys 存储有序的 int 键,mValues 存储值。无哈希表结构,直接通过二分查找定位。
    • 删除元素时仅标记为 DELETED,延迟压缩,减少数组拷贝。
  • 性能优化(特定场景)

    • 二分查找 O(log n),但避免了 HashMap 的哈希冲突问题。
    • 适合频繁查询但低频增删的场景。

适用场景:

  • Key 为 int 类型(如 View 的 ID、资源 ID 等)。
  • 数据量较小(推荐 < 1000)。
  • 需要避免自动装箱(如高性能要求的循环或频繁访问)。

缺点:

  • Key 必须为原始类型(int 或 long)。
  • 数据量大时性能下降明显。

四、与 HashMap 的对比总结

特性HashMapArrayMapSparseArray
内存占用高(Entry 对象 + 哈希表)低(双数组结构)极低(无装箱 + 平行数组)
查找时间复杂度O(1)(平均)O(log n)O(log n)
插入/删除效率高(链表/红黑树处理冲突)低(需移动数组元素)低(延迟删除,需压缩)
自动装箱是(若 Key 为原始类型)是(若 Key 为原始类型)否(Key 为原始类型)
适用数据量大数据量(> 1000)小数据量(< 1000)小数据量(< 1000)

五、实际应用建议

  1. 优先使用 SparseArray

    • 当 Key 为 int 类型时(如 SparseArray<Bitmap> 存储图片资源)。
    • 示例:替换 HashMap<Integer, Object>
  2. 优先使用 ArrayMap

    • 当 Key 为非原始类型且数据量较小时(如 ArrayMap<String, View> 存储视图缓存)。
    • 示例:替换 HashMap<String, Object>
  3. 仍使用 HashMap

    • 数据量较大(> 1000)或需要高频增删操作时。
    • 需要线程安全时(需配合 ConcurrentHashMap)。

通过合理选择数据结构,可以在 Android 应用中显著减少内存占用,提升小数据量场景下的性能表现。

更多分享

  1. 一文带你吃透Android中常见的高效数据结构
  2. 详解:HashMap与TreeMap、HashTable的区别
  3. 详解:Set集合是如何保证元素不重复的
  4. 详解:LinkedHashMap的工作原理和实现
  5. 一文带你搞懂HashSet和TreeSet的区别