SparseArray、SparseIntArray 和 SparseLongArray 的差异

序幕:传统HashMap图书馆的困扰

想象一下,我们有一个巨大的图书馆(内存),里面有很多书架(数组)。最传统的管理方式是 HashMap图书馆

  • 工作方式:每本书(Value)都有一个唯一的、可能是任意数字的索引号(Key)。图书管理员(HashMap)有一个"神奇目录",你告诉他索引号(比如 10001),他能瞬间(O(1)时间)算出这本书在哪个书架的第几个位置。
  • 优点:找书速度极快,无论图书馆多大。
  • 缺点 :为了建立这个"神奇目录",他需要准备很多"目录卡片"(HashMap.Entry 对象)。每张卡片不仅记录了索引号和书的位置,还记录了其他信息以便处理冲突。更麻烦的是,如果索引号是 int 类型,而书是 Integer 这类"精装书",管理员还得给每本"平装书"额外包上一个"精装书皮"(自动装箱),这非常浪费"包装材料"(内存)。

那么,当我们的索引号(Key)本身就是简单的整数(int),而且我们特别在意节省"包装材料"时,有没有更高效的管理方式呢?

答案就是我们的主角:SparseArray系列图书馆


第一幕:SparseArray图书馆 ------ 精益求精的"二分查找"大师

SparseArray 采用了一种截然不同但非常聪明的管理策略。

底层原理:两本紧密协作的登记簿

这位管理员有两本登记簿(两个数组):

  1. mKeys[]: 一本按顺序记录所有图书索引号(Key)的册子。
  2. 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()时)来一次大扫除,把所有被删除的记录彻底清理掉,让两本登记簿重新变得紧凑。

核心优势

  1. 极度节省内存 :因为它没有额外的"目录卡片"(Entry对象),避免了Map的条目开销。对于SparseArray,它的Value是Object,但Key是基本类型int,所以至少避免了Key的自动装箱
  2. 针对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[] 登记簿:

  • SparseArraymValues[] 是一个 Object[] 数组。
  • SparseIntArraymValues[] 是一个 int[] 数组。
  • SparseLongArraymValues[] 是一个 long[] 数组。

优势升华

这意味着,SparseIntArraySparseLongArray 连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) (可能需移动元素)
数据量建议 大小皆可 千级以下 千级以下 千级以下

选择指南:

  1. 什么时候用 SparseArray

    • 当你的Key是int类型,而Value是非基本类型 (如 Object, String, 自定义对象等)时。
    • 例如:Map<Integer, User> 可以用 SparseArray<User> 替代。
  2. 什么时候用 SparseIntArray / SparseLongArray

    • 当你的Key是int类型,且Value正好也是intlong基本类型 时。这是它们的绝对主场,效率最高。
    • 例如:Map<Integer, Integer> (value是resId等) 用 SparseIntArray
    • 例如:Map<Integer, Long>SparseLongArray
  3. 什么时候坚持用 HashMap

    • 数据量非常大(数千以上)时,HashMap的O(1)查找优势会碾压二分查找的O(log n)。
    • Key不是int类型 时(如String, Long等)。Sparse系列只认int key。

核心思想总结

SparseArray家族的本质是:用时间换空间 。它通过二分查找延迟删除 这两种算法,牺牲了一点查询和插入的时间性能,换来了远超HashMap的内存效率 。而SparseIntArraySparseLongArray则是将这个理念发挥到了极致,是特定场景下最极致的优化选择。

所以,下次在Android开发中,如果你的Map键是int类型,不妨先想想:"我是否需要请这几位高效又节省的特种图书馆管理员来帮忙?"

相关推荐
阿巴斯甜1 分钟前
必看11
android
solo_994 分钟前
Perftto 使用命令添加标签
android
阿巴斯甜11 分钟前
必看10
android
阿巴斯甜14 分钟前
必看9
android
阿巴斯甜34 分钟前
必看6
android
angerdream42 分钟前
Android手把手编写儿童手机远程监控App之SQLite详解
android
阿巴斯甜1 小时前
必看5
android
雪铃儿2 小时前
Shorebird 之外,Flutter Android 热更新还有什么选择
android·前端
张筱竼3 小时前
Android开发中的MVC、MVP与MVVM详解
android
阿巴斯甜5 小时前
必看4
android