想象有两个完全不同的图书馆,它们都存储着"书名(键)"和"对应的图书(值)"。
-
图书馆A(HashMap): 这是一个超级现代化的大型图书馆。它有一个巨大的索引厅,里面有成千上万个带编号的小抽屉(数组桶)。当你来借书时,管理员会用一个神奇的公式(哈希函数)
f(书名)
瞬间算出书名对应的抽屉编号,直接走过去打开抽屉,里面放着的就是你要的书。无论图书馆有多少书,他几乎总能一步到位。但每个抽屉本身也需要空间,即使它是空的。 -
图书馆B(ArrayMap): 这是一个小巧精致的社区图书馆。它没有复杂的索引系统,只有两本紧密相连的目录册。
- 第一本目录册(mHashes): 只记录所有书名的"缩写号"(哈希码)。
- 第二本目录册(mArray): 交错地记录着所有书名和图书的具体位置,顺序和第一本目录册完全对应。
现在,让我们化身成为图书馆B(ArrayMap)的管理员,亲身体验一下它的工作流程。
第一幕:核心设计------两本紧密相连的目录册
ArrayMap
的内部核心就是两个数组:
int[] mHashes
: 这本目录册只负责记录所有键(Key)的哈希值。所有键值对都按照键的哈希值从小到大排序。Object[] mArray
: 这本目录册的实际容量是mHashes
的两倍。它以一种交错的方式存储数据:[Key1, Value1, Key2, Value2, Key3, Value3, ...]
。
初始状态: 图书馆刚开张,两本目录册都是空的。
第二幕:添书入库(put操作)------ 高效的整理术
现在,读者要来存三本书:《Cinderella》(灰姑娘),《Beowulf》(贝奥武夫),《Alice》(爱丽丝)。
-
接收请求:
put("Alice", book_alice)
,put("Beowulf", book_beowulf)
,put("Cinderella", book_cinderella)
。 -
计算"缩写号"(哈希码):
hashAlice = "Alice".hashCode()
hashBeowulf = "Beowulf".hashCode()
hashCinderella = "Cinderella".hashCode()
-
排序与插入(二分查找的妙用):
这是
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》这本书。
- 计算哈希码:
hashAlice = "Alice".hashCode()
- 二分查找: 管理员在
mHashes
这本有序目录册中,使用高效的二分查找,快速定位到hashAlice
所在的索引位置,比如 index=1。 - 最终确认与取值: 找到索引后,管理员还要做最后一步验证:去
mArray[ index * 2 ]
(即mArray[2]
)的位置看一下,这里存的名字是不是确实是"Alice"
?(这是为了处理哈希冲突,确保键是真正的相等而不仅仅是哈希值相等)。 - 取出书籍: 确认无误后,从
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
的扩容策略非常克制和聪明。
- 当前容量不够时 ,它会选择一个新容量。它的扩容不是像
HashMap
那样直接翻倍,而是有一个更平滑的增长曲线(例如,当前容量小于4则扩为4,小于8则扩为8,超过8则扩容1.5倍)。这进一步减少了内存浪费。 - 创建新的、更大容量的
mHashes
和mArray
。 - 将旧数组的数据有序地拷贝到新数组中。
拆除(删除操作) 也类似,删除后如果需要,还会进行数组的缩容,以节省空间。
故事总结与使用场景指南
现在你明白 ArrayMap
这个"精致社区图书馆"的运作方式了。我们来总结一下它的特点和最佳使用场景:
底层原理精髓:
- 双数组结构: 一个存哈希码(有序),一个交错存键值对。极其节省内存。
- 二分查找: 通过维护哈希码数组的有序性,实现了高效的查找和插入定位(O(log N))。
- 缓扩容,爱缩容: 扩容策略比
HashMap
更保守,更注重节省内存空间。
最佳使用场景(什么时候选择图书馆B):
- 数据量小(核心场景): 通常是少于1000个,特别是几百个键值对的情况。这是它的主战场。在数据量小时,O(log N) 的查询效率可以接受,而内存优势巨大。
- 频繁拷贝与创建: 例如,在 Android 中,
Intent
的Extras
、Bundle
就大量使用了ArrayMap
。想象一下启动一个 Activity 需要传递参数,这个过程可能会频繁创建和销毁这些集合,使用ArrayMap
能极大减轻内存压力和GC(垃圾回收)负担。 - 内存敏感的环境: 移动设备(Android)是
ArrayMap
的天然家园。在有限的手机内存上,每一个KB都值得珍惜。 - 作为替代品: 你可以用它来替代频繁创建的
HashMap<String, Object>
或HashMap<Integer, Object>
。
需要避免的场景(什么时候还是得去大型图书馆A):
- 数据量大: 当你有成千上万个元素时,
ArrayMap
的 O(log N) 查询性能会明显慢于HashMap
的 O(1),此时性能差距会被放大,内存优势则被削弱。 - 要求极致性能: 如果你的代码处在性能关键路径上,需要毫秒甚至微秒级的响应,
HashMap
仍然是更安全的选择。
总而言之,ArrayMap
是"空间换时间"的 HashMap
的一个完美补充,它采用"时间换空间"的策略,在特定场景(小数据量、移动平台)下做到了资源利用的最优化,体现了工程上的精巧权衡。