Android 集合探秘:ArrayMap 与 SparseArray 的奇妙之旅

14 阅读11分钟

Android 集合探秘:ArrayMap 与 SparseArray 的奇妙之旅

引言:集合世界的新成员

在 Android 开发的奇妙世界里,我们常常与各种集合类打交道。就像在一个大型超市里,不同的商品需要存放在不同的货架区域一样,数据也需要根据其特点和使用场景,被妥善地放置在合适的数据结构中。最常见的 HashMap,它就像一个大型的综合货架,能容纳各种类型的 “商品”(键值对),通过哈希算法可以快速地找到你想要的 “商品”,查找速度非常快,适用于大规模数据的存储和快速检索。

但今天,我们要聚焦在两个不太一样的 “货架” 上 ——ArrayMap 和 SparseArray。它们在 Android 开发中有着独特的地位,就像是超市里那些专门为特定商品设置的特色货架。ArrayMap 和 SparseArray 是为了应对 Android 系统中一些特殊的需求而诞生的,它们在某些场景下比传统的集合类表现得更加出色。那为什么在已经有了 ArrayMap 的情况下,还需要设计 SparseArray 呢?它们之间到底有什么区别和联系?这就像是超市里的两种特色货架,看起来有些相似,却又各有各的用途,接下来就让我们一起深入探究一下。

一、ArrayMap:通用的内存优化能手

(一)结构与原理

ArrayMap 就像是一个精心整理的小型仓库,虽然规模不大,但布局十分巧妙。它内部使用了两个数组来存储数据,一个是int[] mHashes数组,用来存储所有键(key)的哈希值,并且这些哈希值是按照从小到大的顺序排列的;另一个是Object[] mArray数组,它的长度是mHashes数组的两倍,以交错的方式存储键值对,即第 0 个位置存储第一个键,第 1 个位置存储第一个值,第 2 个位置存储第二个键,第 3 个位置存储第二个值,以此类推 。

当我们向 ArrayMap 中插入一个键值对时,它会先计算键的哈希值,然后通过二分查找法在mHashes数组中寻找合适的插入位置。二分查找法就像是在一本按字母顺序排列的字典中查找单词,通过不断地将查找范围缩小一半,能快速地找到目标位置。如果找到了相同哈希值的位置,还会进一步比较键是否相等,以确定是更新值还是插入新的键值对。

在查找元素时,同样是先计算键的哈希值,利用二分查找在mHashes数组中定位,找到哈希值对应的位置后,再在mArray数组中获取对应的值。这种设计使得 ArrayMap 在数据量较小时,能够高效地进行插入、删除和查找操作,并且由于它避免了像 HashMap 那样为每个键值对创建单独的节点对象,大大节省了内存空间。

(二)适用场景

ArrayMap 适用于存储少量对象类型键值对的场景。比如在一个简单的用户信息管理模块中,我们可能只需要存储用户的姓名、年龄、性别等少量信息,这时使用 ArrayMap 就非常合适。它可以将这些信息以键值对的形式存储起来,并且占用的内存空间比 HashMap 要小很多。

再比如,在一个配置项管理系统中,我们可能会存储一些系统的配置参数,如是否开启音效、是否显示通知等,这些配置项的数量通常不会太多,使用 ArrayMap 可以有效地管理这些配置信息,并且在需要频繁读取这些配置项时,ArrayMap 的遍历操作效率也比较高。因为它是顺序遍历数组,对缓存更加友好,能够快速地获取到每个键值对。

二、SparseArray:整型键的专属利器

(一)独特设计

SparseArray 则像是一个专门为整型 “商品” 打造的精致小货架,有着别具一格的设计。它内部使用了两个数组来存储数据,一个是int[] mKeys数组,专门用来存储整型的键(key),这些键是按照从小到大的顺序排列的;另一个是Object[] mValues数组,用于存储与键相对应的值(value) 。

当我们往 SparseArray 中添加一个键值对时,它会先通过二分查找法在mKeys数组中查找合适的插入位置,以保证mKeys数组始终有序。二分查找法就像是在一本按页码顺序排列的图书中查找特定页码,每次都能将查找范围缩小一半,从而快速定位。找到位置后,就在mKeys数组的相应位置插入键,同时在mValues数组的对应位置插入值。

在查找元素时,同样是利用二分查找在mKeys数组中找到键的位置,然后在mValues数组中取出对应的值。这种设计最大的特点就是避免了装箱操作,因为它直接使用基本数据类型 int 作为键,而不需要像 HashMap 那样将 int 类型的键装箱成 Integer 对象,大大节省了内存和 CPU 资源。 而且,由于键是有序存储的,在某些场景下,比如需要按顺序遍历键值对时,SparseArray 的表现会更加出色。

(二)应用场景举例

SparseArray 适用于键为整型且数据量较小的场景。在 Android 开发中,一个常见的应用场景是将视图 ID(View ID)映射到对应的视图对象。每个视图在布局文件中都有一个唯一的整型 ID,例如R.id.button1,我们可以使用 SparseArray 来存储这些视图 ID 和对应的视图对象。


SparseArray<View> viewSparseArray = new SparseArray<>();
viewSparseArray.put(R.id.button1, findViewById(R.id.button1));
viewSparseArray.put(R.id.textView, findViewById(R.id.textView));
// 查找视图
View button = viewSparseArray.get(R.id.button1);

这样做不仅比使用 HashMap 更加节省内存,而且在查找视图时,利用 SparseArray 的二分查找特性,速度也相当快。

再比如,在一个简单的游戏开发中,我们可能需要存储玩家在不同关卡的得分情况,关卡编号通常是整型的。这时使用 SparseArray 来存储关卡编号和对应的得分,就非常合适。


SparseArray<Integer> scoreSparseArray = new SparseArray<>();
scoreSparseArray.put(1, 100);
scoreSparseArray.put(2, 200);
// 获取第二关的得分
int score = scoreSparseArray.get(2);

在这个例子中,如果使用 HashMap,会因为将整型的关卡编号装箱成 Integer 对象而浪费内存,而 SparseArray 则可以避免这个问题,同时在数据量不大的情况下,其查找和插入操作的性能也能满足需求。

三、两者对比:差异决定选择

现在我们已经了解了 ArrayMap 和 SparseArray 各自的特点和适用场景,接下来就来深入对比一下它们,看看在不同的维度下,它们各自有着怎样的表现。

(一)内存占用

在内存占用方面,SparseArray 堪称 “内存小能手” 。因为它直接使用 int 类型数组存储键,避免了装箱操作,也就没有了将基本数据类型转换为对象时所带来的额外内存开销。而且它没有像 HashMap 那样每个键值对都需要创建一个单独的 Entry 对象,大大减少了内存的使用。相比之下,ArrayMap 虽然也通过两个数组来存储数据,避免了像 HashMap 那样为每个键值对创建单独节点对象的开销,但当键为对象类型时,还是会存在一定的内存占用,因为对象本身需要占用内存空间,并且它还需要一个数组来存储键的哈希值。所以在内存占用上,SparseArray 要优于 ArrayMap,尤其是当键为整型且数据量较大时,这种优势更加明显。

(二)查找效率

从查找效率来看,ArrayMap 和 SparseArray 都采用了二分查找法来定位元素。当数据量较小时,两者的查找效率相差不大,都能快速地找到目标元素,时间复杂度均为 O (log n) 。然而,当数据量逐渐增大时,由于 ArrayMap 需要先计算键的哈希值,再通过哈希值在哈希数组中进行二分查找,这个过程会带来一定的时间开销。而 SparseArray 直接在有序的整型键数组中进行二分查找,相对来说更加直接。不过总体而言,在数据量不是特别巨大的情况下,它们的查找效率都能满足大多数场景的需求。

(三)插入删除效率

在插入和删除操作上,两者的表现都不算特别出色 。ArrayMap 插入新元素时,需要在哈希数组中找到合适的位置插入哈希值,同时在键值数组中插入对应的键值对,并且可能需要移动数组元素来保持有序性,时间复杂度为 O (n)。删除元素时同样需要移动数组元素,效率较低。SparseArray 插入元素时,要在有序的键数组中找到合适位置插入键和对应的值,可能需要移动数组元素,时间复杂度为 O (n) 。不过,SparseArray 有一个特殊的延迟回收机制,删除元素时只是将其标记为 “已删除”,并不会立即移动数组元素,而是在后续插入操作或主动调用gc()方法时才进行数组压缩,这在一定程度上减少了频繁移动数组元素带来的开销,所以在频繁插入和删除操作的场景下,SparseArray 的表现会略好于 ArrayMap。

(四)适用数据量

ArrayMap 和 SparseArray 都更适合存储小到中等规模的数据,一般建议数据量在千级以内 。当数据量超过这个范围时,随着数组的不断扩容以及插入删除操作导致的数组元素频繁移动,它们的性能会逐渐下降,此时使用 HashMap 可能是更好的选择。因为 HashMap 在大数据量下,虽然存在哈希冲突和扩容等问题,但它的平均查找时间复杂度为 O (1),在查找性能上更具优势。

(五)键类型

键类型的选择是区分 ArrayMap 和 SparseArray 的关键因素 。ArrayMap 可以存储任意类型的键,包括 null,这使得它在处理各种类型的数据时非常灵活。比如我们可以用它来存储字符串类型的键值对,如配置参数信息等。而 SparseArray 只能存储 int 类型的键,这就限制了它的使用场景,只能在键为整型的情况下发挥其优势,比如存储视图 ID 和对应的视图对象。

四、总结:各有所长,按需而用

通过前面的深入分析,我们对 ArrayMap 和 SparseArray 有了全面的了解。ArrayMap 像是一位通用的 “内存优化大师”,在数据量较小且键为对象类型时,凭借其巧妙的双数组结构和独特的哈希值管理方式,能高效地存储和管理数据,节省内存的同时,还能保证一定的操作效率,特别适合存储配置信息、少量的用户数据等场景。

而 SparseArray 则是为整型键值对量身定制的 “专属专家”,它利用有序的整型键数组和对应的值数组,避免了装箱操作,在内存占用上表现出色,尤其是在键为整型且数据量不大的情况下,如存储视图 ID 与视图对象的映射、游戏关卡得分等场景,有着独特的优势。

在实际的 Android 开发中,我们就像在一个充满选择的 “数据结构超市” 里挑选最合适的工具。当面对不同的业务需求时,要综合考虑数据量大小、键的类型、内存占用以及操作效率等因素 ,选择最适合的集合类。比如,当数据量较大时,HashMap 凭借其高效的查找性能可能是更好的选择;但如果对内存非常敏感,且数据量较小,那么 ArrayMap 和 SparseArray 就能发挥它们的特长,帮助我们优化应用的性能和内存管理。希望大家在今后的开发中,都能巧妙地运用这些集合类,打造出更加高效、流畅的 Android 应用。