故事:两个图书馆的比喻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 的一个完美补充,它采用"时间换空间"的策略,在特定场景(小数据量、移动平台)下做到了资源利用的最优化,体现了工程上的精巧权衡。

相关推荐
simplepeng14 分钟前
Room 3.0 KMP Alpha-01
android·kotlin·android jetpack
Lei活在当下28 分钟前
Windows 下 Codex 高效工作流最佳实践
android·openai·ai编程
fatiaozhang952728 分钟前
基于slimBOXtv 9.19.0 v4(通刷晶晨S905L3A/L3AB芯片)ATV-安卓9-完美版线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·晶晨s905l3ab·晶晨s905l3a
私房菜2 小时前
Selinux 及在Android 的使用详解
android·selinux·sepolicy
一只特立独行的Yang2 小时前
Android中的系统级共享库
android
两个人的幸福online3 小时前
php开发者 需要 协程吗
android·开发语言·php
修炼者4 小时前
WindowManager(WMS)构建全局悬浮窗
android
xiaoshiquan12064 小时前
Android Studio里,SDK Manager显示不全问题
android·ide·android studio
Lstone73645 小时前
Bitmap深入分析(一)
android
一起搞IT吧6 小时前
Android功耗系列专题理论之十四:Sensor功耗问题分析方法
android·c++·智能手机·性能优化