故事:两个图书馆的比喻ArrayMap

想象有两个完全不同的图书馆,它们都存储着"书名(键)"和"对应的图书(值)"。

  1. 图书馆A(HashMap): 这是一个超级现代化的大型图书馆。它有一个巨大的索引厅,里面有成千上万个带编号的小抽屉(数组桶)。当你来借书时,管理员会用一个神奇的公式(哈希函数)f(书名) 瞬间算出书名对应的抽屉编号,直接走过去打开抽屉,里面放着的就是你要的书。无论图书馆有多少书,他几乎总能一步到位。但每个抽屉本身也需要空间,即使它是空的。

  2. 图书馆B(ArrayMap): 这是一个小巧精致的社区图书馆。它没有复杂的索引系统,只有两本紧密相连的目录册。

    • 第一本目录册(mHashes): 只记录所有书名的"缩写号"(哈希码)。
    • 第二本目录册(mArray): 交错地记录着所有书名和图书的具体位置,顺序和第一本目录册完全对应。

现在,让我们化身成为图书馆B(ArrayMap)的管理员,亲身体验一下它的工作流程。


第一幕:核心设计------两本紧密相连的目录册

ArrayMap 的内部核心就是两个数组:

  • int[] mHashes: 这本目录册只负责记录所有键(Key)的哈希值。所有键值对都按照键的哈希值从小到大排序
  • Object[] mArray: 这本目录册的实际容量是 mHashes 的两倍。它以一种交错的方式存储数据:[Key1, Value1, Key2, Value2, Key3, Value3, ...]

初始状态: 图书馆刚开张,两本目录册都是空的。

第二幕:添书入库(put操作)------ 高效的整理术

现在,读者要来存三本书:《Cinderella》(灰姑娘),《Beowulf》(贝奥武夫),《Alice》(爱丽丝)。

  1. 接收请求: put("Alice", book_alice), put("Beowulf", book_beowulf), put("Cinderella", book_cinderella)

  2. 计算"缩写号"(哈希码):

    • hashAlice = "Alice".hashCode()
    • hashBeowulf = "Beowulf".hashCode()
    • hashCinderella = "Cinderella".hashCode()
  3. 排序与插入(二分查找的妙用):

    这是 ArrayMap 最精妙的一步!我们不是简单地把新书放到最后,而是时刻保持 mHashes 数组是有序的

    • 每当要放入一个新键值对时,管理员会用二分查找ContainerHelpers.binarySearch)在 mHashes 数组中快速找到这个哈希码应该被插入的位置,以保证数组有序。
    • 比如,假设三个哈希值的大小关系是:hashBeowulf < hashAlice < hashCinderella
    • 管理员会在 mHashes 中找到正确的位置并插入它们,最终 mHashes 变为:[hashBeowulf, hashAlice, hashCinderella]
    • 同时,在 mArray 中,他会在对应位置(索引 * 2 放键,索引 * 2 + 1 放值)交错地插入键和值。所以 mArray 最终为:
      [0: "Beowulf", 1: book_beowulf, 2: "Alice", 3: book_alice, 4: "Cinderella", 5: book_cinderella]

这样做的好处是什么?

极致的内存节省和合理的查询效率。ArrayMap 只使用了两个非常紧凑的数组,没有任何多余的链表或指针开销(不像 HashMap 的Entry节点)。对于几百个元素以内的集合,这种结构在内存上非常经济。

第三幕:借书查询(get操作)------ 先快后慢的查找

读者来借《Alice》这本书。

  1. 计算哈希码: hashAlice = "Alice".hashCode()
  2. 二分查找: 管理员在 mHashes 这本有序目录册中,使用高效的二分查找,快速定位到 hashAlice 所在的索引位置,比如 index=1。
  3. 最终确认与取值: 找到索引后,管理员还要做最后一步验证:去 mArray[ index * 2 ](即 mArray[2])的位置看一下,这里存的名字是不是确实是 "Alice"?(这是为了处理哈希冲突,确保键是真正的相等而不仅仅是哈希值相等)。
  4. 取出书籍: 确认无误后,从 mArray[ index * 2 + 1 ](即 mArray[3])取出 book_alice 交给读者。

性能分析:

  • 二分查找(O(log N)) :查找哈希码的位置很快。
  • 最终验证(O(1)) :根据索引直接取值。
    所以,总的查询时间复杂度是 O(log N) 。这比 HashMap 理想的 O(1) 要慢,但当 N 很小(比如几十到几百)时,O(log N) 和 O(1) 的实际时间差距微乎其微,完全可以用微小的性能损失换来巨大的内存优势。

第四幕:拆除与扩建(扩容机制)

图书馆的书越来越多了,原来的目录册写满了怎么办?

ArrayMap 的扩容策略非常克制和聪明。

  1. 当前容量不够时 ,它会选择一个新容量。它的扩容不是像 HashMap 那样直接翻倍,而是有一个更平滑的增长曲线(例如,当前容量小于4则扩为4,小于8则扩为8,超过8则扩容1.5倍)。这进一步减少了内存浪费。
  2. 创建新的、更大容量的 mHashesmArray
  3. 将旧数组的数据有序地拷贝到新数组中。

拆除(删除操作) 也类似,删除后如果需要,还会进行数组的缩容,以节省空间。


故事总结与使用场景指南

现在你明白 ArrayMap 这个"精致社区图书馆"的运作方式了。我们来总结一下它的特点和最佳使用场景:

底层原理精髓:

  1. 双数组结构: 一个存哈希码(有序),一个交错存键值对。极其节省内存。
  2. 二分查找: 通过维护哈希码数组的有序性,实现了高效的查找和插入定位(O(log N))。
  3. 缓扩容,爱缩容: 扩容策略比 HashMap 更保守,更注重节省内存空间。

最佳使用场景(什么时候选择图书馆B):

  1. 数据量小(核心场景): 通常是少于1000个,特别是几百个键值对的情况。这是它的主战场。在数据量小时,O(log N) 的查询效率可以接受,而内存优势巨大。
  2. 频繁拷贝与创建: 例如,在 Android 中,IntentExtrasBundle 就大量使用了 ArrayMap。想象一下启动一个 Activity 需要传递参数,这个过程可能会频繁创建和销毁这些集合,使用 ArrayMap 能极大减轻内存压力和GC(垃圾回收)负担。
  3. 内存敏感的环境: 移动设备(Android)是 ArrayMap 的天然家园。在有限的手机内存上,每一个KB都值得珍惜。
  4. 作为替代品: 你可以用它来替代频繁创建的 HashMap<String, Object>HashMap<Integer, Object>

需要避免的场景(什么时候还是得去大型图书馆A):

  1. 数据量大: 当你有成千上万个元素时,ArrayMap 的 O(log N) 查询性能会明显慢于 HashMap 的 O(1),此时性能差距会被放大,内存优势则被削弱。
  2. 要求极致性能: 如果你的代码处在性能关键路径上,需要毫秒甚至微秒级的响应,HashMap 仍然是更安全的选择。

总而言之,ArrayMap 是"空间换时间"的 HashMap 的一个完美补充,它采用"时间换空间"的策略,在特定场景(小数据量、移动平台)下做到了资源利用的最优化,体现了工程上的精巧权衡。

相关推荐
用户2018792831672 小时前
SparseArray、SparseIntArray 和 SparseLongArray 的差异
android
2501_916013743 小时前
App 上架全流程指南,iOS App 上架步骤、App Store 应用发布流程、uni-app 打包上传与审核要点详解
android·ios·小程序·https·uni-app·iphone·webview
牛蛙点点申请出战3 小时前
仿微信语音 WaveView 实现
android·前端·ios
用户093 小时前
Android View 事件分发机制详解及应用
android·kotlin
ForteScarlet3 小时前
Kotlin 2.2.20 现已发布!下个版本的特性抢先看!
android·开发语言·kotlin·jetbrains
诺诺Okami3 小时前
Android Framework-Input-8 ANR相关
android
法欧特斯卡雷特3 小时前
Kotlin 2.2.20 现已发布!下个版本的特性抢先看!
android·前端·后端
人生游戏牛马NPC1号3 小时前
学习 Android (二十一) 学习 OpenCV (六)
android·opencv·学习
用户2018792831673 小时前
Native 层 Handler 机制与 Java 层共用 MessageQueue 的设计逻辑
android