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

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

相关推荐
alexhilton4 小时前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Cosolar4 小时前
FunASR 前端语音识别代码解析
前端·面试·github
kobe_OKOK_5 小时前
Django `models.Field` 所有常见配置参数的完整清单与说明表
android
躬身入世,以生证道6 小时前
面试技术栈 —— 简历篇
面试·职场和发展
前行的小黑炭7 小时前
Android Compose :初步了解一下生命周期,对比原生android
android·kotlin·app
湖南人爱科技有限公司8 小时前
RaPhp和Python某音最新bd-ticket-guard-client-data加密算法解析(视频评论)
android·python·php·音视频·爬山算法·raphp
程序员飞哥8 小时前
如何设计多级缓存架构并解决一致性问题?
java·后端·面试
道可到11 小时前
百度面试真题 Java 面试通关笔记 04 |JMM 与 Happens-Before并发正确性的基石(面试可复述版)
java·后端·面试
守城小轩12 小时前
Chromium 138 编译指南 - Android 篇:从Linux版切换到Android版(六)
android·chrome·指纹浏览器·浏览器开发·超级浏览器
bot55566612 小时前
“企业微信iPad协议”静默 72 小时:一台被遗忘的测试机如何成为私域的逃生梯
javascript·面试