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

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

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

一句话总结选择策略:

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

一、核心区别总览

特性 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 好)。
    • ⚠️ 注意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 类型 (例如 viewId, resourceId, 数据库主键 _id),就应优先考虑 SparseArray
    • ✅ 适用于数据量不大的场景(千级以下)。

SparseArray 家族变体:

  • SparseIntArray : Keyint, Valueint。用于替代 HashMap<Integer, Integer>
  • SparseLongArray : Keyint, Valuelong
  • LongSparseArray : Keylong, ValueObject。用于替代 HashMap<Long, Object>
  • SparseBooleanArray : Keyint, Valueboolean

三、实战代码示例与对比

假设有一个场景:用 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

总结黄金法则:

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

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

相关推荐
CYRUS_STUDIO1 小时前
Frida Stalker Trace 实战:指令级跟踪与寄存器变化监控全解析
android·逆向
小高0071 小时前
🌐ES6 这 8 个隐藏外挂,知道 3 个算我输!
前端·javascript·面试
甜瓜看代码2 小时前
Android事件分发机制
面试
李重楼3 小时前
前端性能优化之 HTTP/2 多路复用
前端·面试
lecepin4 小时前
AI Coding 资讯 2025-09-17
前端·javascript·面试
闰五月6 小时前
JavaScript作用域与作用域链详解
前端·面试
ace望世界6 小时前
android的Parcelable
android
顾林海6 小时前
Android编译插桩之AspectJ:让代码像特工一样悄悄干活
android·面试·性能优化
poemyang6 小时前
技术圈的“绯闻女孩”:Gossip是如何把八卦秘密传遍全网的?
后端·面试·架构