[TOC]
1. 为什么要用ArrayMap/ArraySet
在Android开发中,经常会用到HashMap/HashSet等集合类,但是Java在设计集合类的时候并没有考虑到内存宝贵场景下优化。而对Android系统来说内存是非常宝贵的资源,所以Google针对Android系统的特性提供了 HashMap/HashSet的代替品,即ArrayMap/ArraySet。这几个类位于android.util.*包下面。
2. 简单回忆下HashMap<K, V>的实现
2.1 数据接口
在Java中,HashMap是通过数组加引用组合实现的数据结构,结构图如下:
)
从图中我们看到,数组的每个元素之后会跟着一条链表。我们应该很容易想到这是为了解决HashCode冲突而设计的,使用链地址法解决冲突。
顺便提一下解决冲突的常用四个方法:
- 链地址法
 - 再Hash法
 - 开放地址发
 - 建立公共溢出区
 
2.2 HashMap的数据结构
HashMap有三个构造方法:
1  | HaspMap()  | 
- 其中 initialCapacity是一个初始化容量大小,指的是table数组的初始大小。
 - loadFactor是负载因子,当table数组的实际容量超过(initialCapacity*loadFactor)的时候,table数组就会扩容。
 - initialCapacity默认值是15 而loadFactor的默认值是0.75 他俩都是影响HashMap性能的重要因素。
 
首先看下构造函数的源码:
1  | public HashMap(int initialCapacity, float loadFactor) {  | 
其中有一个Entry是HashMap的内部类:
1  | Entry() {  | 
它是实际储存Key和Value的实体。next是指向下一个元素的引用,hash是key的hash值。
2.3 数据的存放
再来看下put方法的源码:
1  | public V put(K key, V value) {  | 
- 首先判断key值是否为null,如果是null,则将hash值当成0处理 否则
 - 根据key对象的
hashCode()方法计算出hash值,然后再将hash值转化成在table中的索引 然后 - 查找该索引处的链表中是否存在key相同的对象,如果有则将其覆盖,并且返回旧值 否则
 - 将key、value存放于该数组索引处链表的第一个位置上,其中这一步是在
addEntry()方法中完成的 
1  | void addEntry(int hash, K key, V value, int bucketIndex) {  | 
2.4 数据的取出
对于HashMap,取出值是相对简单的:
1  | public V get(Object key) {  | 
2.5 HashMap的小结
从上面的分析中可以看出,HashMap的存取速度都是相对比较快的,在一般情况下都能实现O(1)的速度,但是从初始容量和负载因子都可以看出,这种快速的读取都是通过内存来换取,而对于移动设备来讲,内存又是很重要的,所以,Google为我们提供了Arraymap来代替它。
3. ArrayMap的简单分析
在ArrayMap的内部,使用一个hash数组加一个kay,value数组来储存。

当你想获取某个value的时候,ArrayMap会计算输入key转换过后的hash值,然后对hash数组使用二分查找法寻找到对应的index,然后我们可以通过这个index在另外一个数组中直接访问到需要的键值对。如果在第二个数组键值对中的key和前面输入的查询key不一致,那么就认为是发生了碰撞冲突。为了解决这个问题,我们会以该key为中心点,分别上下展开,逐个去对比查找,直到找到匹配的值(开放地址法)。如下图所示:

可以看出,ArrayMap将内存的使用率提高了很多,但是读取的复杂度却是O(lgN)(因为二分法查找)。所以在数据量不是很大(千级以内)的时候,我们使用ArrapMap可以优化内存,并且存取速度几乎是不受什么影响的。
其实这里还有一点,就是自动装箱问题,假设我们把一个key是int类型的数据存储时,HashMap会将int值自动装箱成Integer对象,一个对象和一个基本类型所占的内存大小是差别很大的,所以为了避免这样情况,Google提供了SparseIntArray,SparseBoolArray等一系列大礼包供我们使用。
4. 总结
总结一句话,在数据量千级以内,使用ArrayMap ArraySet或者SparseIntMap等来代替HashMap,以此节约宝贵的内存资源。