序幕:传统HashMap图书馆的困扰
想象一下,我们有一个巨大的图书馆(内存),里面有很多书架(数组)。最传统的管理方式是 HashMap图书馆:
- 工作方式:每本书(Value)都有一个唯一的、可能是任意数字的索引号(Key)。图书管理员(HashMap)有一个"神奇目录",你告诉他索引号(比如 10001),他能瞬间(O(1)时间)算出这本书在哪个书架的第几个位置。
- 优点:找书速度极快,无论图书馆多大。
- 缺点 :为了建立这个"神奇目录",他需要准备很多"目录卡片"(
HashMap.Entry
对象)。每张卡片不仅记录了索引号和书的位置,还记录了其他信息以便处理冲突。更麻烦的是,如果索引号是int
类型,而书是Integer
这类"精装书",管理员还得给每本"平装书"额外包上一个"精装书皮"(自动装箱),这非常浪费"包装材料"(内存)。
那么,当我们的索引号(Key)本身就是简单的整数(int
),而且我们特别在意节省"包装材料"时,有没有更高效的管理方式呢?
答案就是我们的主角:SparseArray系列图书馆。
第一幕:SparseArray图书馆 ------ 精益求精的"二分查找"大师
SparseArray
采用了一种截然不同但非常聪明的管理策略。
底层原理:两本紧密协作的登记簿
这位管理员有两本登记簿(两个数组):
mKeys[]
: 一本按顺序记录所有图书索引号(Key)的册子。mValues[]
: 另一本册子,严格地、一对一地记录着对应索引号的图书(Value)所在的具体位置。
工作流程("增删查改"的故事)
-
查(
get
) :当你来找索引号为10001
的书时,管理员不会用"神奇目录",而是会打开mKeys[]
登记簿,使用二分查找法 快速地翻阅。因为登记簿是有序的,他很快就能确定10001
这个号在不在簿子上,以及在第几行(索引位置)。一旦找到行数,他立刻去mValues[]
登记簿的同一行,找到书的存放位置。- 专家解读:查找时间复杂度是 O(log n),对于数据量小(例如千级以内)的情况,这和 HashMap 的 O(1) 差距微乎其微,但却换来了巨大的内存节省。
-
增(
put
) :你要存入一本索引号为10001
的新书。管理员同样会用二分查找法确定10001
这个号应该按顺序插入到mKeys[]
登记簿的哪个位置(比如第5行和第6行之间)。然后,他会在mKeys[]
和mValues[]
的同一个位置 (比如第6行)同时插入新的索引号和图书信息。这可能需要移动后面所有的记录,所以他更擅长处理不频繁插入的情况。 -
删(
delete
) :这里是最精妙的设计!当需要删除一本书时,管理员并不会立刻把登记簿上的这条记录划掉并把后面的记录前移(那太耗时了)。他有一个更巧妙的办法:他只是在mValues[]
登记簿的那一行上,贴一个特殊的标签------DELETED
。- 这行记录就变成了一个"空位"。下次要插入新书时,他会优先复用这些贴有
DELETED
标签的空位,而不是去登记簿末尾添加新行。这避免了频繁的数组拷贝。 - 当然,如果空位太多,他也会在合适的时候(比如垃圾回收
gc()
时)来一次大扫除,把所有被删除的记录彻底清理掉,让两本登记簿重新变得紧凑。
- 这行记录就变成了一个"空位"。下次要插入新书时,他会优先复用这些贴有
核心优势
- 极度节省内存 :因为它没有额外的"目录卡片"(Entry对象),避免了Map的条目开销。对于
SparseArray
,它的Value是Object
,但Key是基本类型int
,所以至少避免了Key的自动装箱。 - 针对Android优化 :在早期Android设备上,内存非常宝贵,GC(垃圾回收)对应用性能影响很大。
SparseArray
的延迟删除机制(标记DELETED
)减少了数组结构的修改,从而减少了触发GC的次数,使应用更流畅。
第二幕 & 第三幕:SparseIntArray & SparseLongArray ------ 专一高效的"特种"图书馆
SparseArray
已经很棒了,但它的 mValues[]
登记簿记录的是"图书的位置"(Object
类型)。这意味着,如果你存的本来就是一本"平装书"(比如 int
, long
这些基本类型),管理员还是得给它包上一个"精装书皮"(自动装箱 成 Integer
, Long
),这又产生了不必要的浪费。
于是,两位更专一的专家登场了:
SparseIntArray
: 它专门管理 索引号(Key)是int
,图书(Value)也是int
的图书馆。SparseLongArray
: 它专门管理 索引号(Key)是int
,图书(Value)是long
的图书馆。
底层原理差异
它们的核心思想和工作流程(二分查找、延迟删除)和 SparseArray
完全一样。
唯一的、也是最重要的区别 在于那本 mValues[]
登记簿:
SparseArray
的mValues[]
是一个Object[]
数组。SparseIntArray
的mValues[]
是一个int[]
数组。SparseLongArray
的mValues[]
是一个long[]
数组。
优势升华
这意味着,SparseIntArray
和 SparseLongArray
连Value的自动装箱也彻底避免了!它们直接用原生数组存储值,内存利用率达到了极致。
SparseArray
: 避免了Key的装箱。SparseIntArray
: 同时避免了Key和Value的装箱。
终幕:如何选择你的图书馆管理员?(使用场景总结)
特性 | HashMap<Integer, Object> |
SparseArray<Object> |
SparseIntArray |
SparseLongArray |
---|---|---|---|---|
Key类型 | Integer (装箱) |
int (基本类型) |
int (基本类型) |
int (基本类型) |
Value类型 | Object |
Object |
int (基本类型) |
long (基本类型) |
内存开销 | 高 (有Entry对象,Key+Value可能双装箱) | 中 (无Entry,Key无装箱) | 极低 (无Entry,Key+Value均无装箱) | 极低 (无Entry,Key+Value均无装箱) |
查找效率 | O(1) (极快) | O(log n) (快,小数据量下无感) | O(log n) (快,小数据量下无感) | O(log n) (快,小数据量下无感) |
插入/删除效率 | O(1) / O(1) | O(n) (可能需移动元素) | O(n) (可能需移动元素) | O(n) (可能需移动元素) |
数据量建议 | 大小皆可 | 千级以下 | 千级以下 | 千级以下 |
选择指南:
-
什么时候用
SparseArray
?- 当你的Key是
int
类型,而Value是非基本类型 (如Object
,String
, 自定义对象等)时。 - 例如:
Map<Integer, User>
可以用SparseArray<User>
替代。
- 当你的Key是
-
什么时候用
SparseIntArray
/SparseLongArray
?- 当你的Key是
int
类型,且Value正好也是int
或long
基本类型 时。这是它们的绝对主场,效率最高。 - 例如:
Map<Integer, Integer>
(value是resId等) 用SparseIntArray
。 - 例如:
Map<Integer, Long>
用SparseLongArray
。
- 当你的Key是
-
什么时候坚持用
HashMap
?- 数据量非常大(数千以上)时,HashMap的O(1)查找优势会碾压二分查找的O(log n)。
- Key不是
int
类型 时(如String
,Long
等)。Sparse系列只认int
key。
核心思想总结
SparseArray家族的本质是:用时间换空间 。它通过二分查找 和延迟删除 这两种算法,牺牲了一点查询和插入的时间性能,换来了远超HashMap的内存效率 。而SparseIntArray
和SparseLongArray
则是将这个理念发挥到了极致,是特定场景下最极致的优化选择。
所以,下次在Android开发中,如果你的Map键是int类型,不妨先想想:"我是否需要请这几位高效又节省的特种图书馆管理员来帮忙?"