SparseArray & ArrayMap

SparseArrayArrayMap 都是 Android 特有的集合类,出现在 android.util 包下。

它们的诞生原因是 Android 在内存和性能优化上的特殊需求 ------ 在移动设备上,内存、GC 频率和性能都是敏感指标,所以 Google 给我们提供了比 Java 标准集合更轻量的替代方案。

它们在功能上与 Java 标准库中的 HashMap 类似,都是键值对(Key-Value)存储,但在内部实现上做了显著的改进。

一、SparseArray

SparseArray 是一种将整数映射到对象的键值对数据结构。它的设计初衷是为了替代 HashMap<Integer, Object>,特别是在键是连续或稀疏的整数时。

它在系统源码和三方库中很常见,但因没有去了解过它,以至于很长时间都不知道它是干什么用的,只知道看上去和 HashMap 很像。

特点:两个数组(key 升序排列,value 对应存储)

vbnet 复制代码
mKeys:   [  1,   3,   5  ]
mValues: ["A", "B", "C"]

查找流程:
1. 用二分查找 mKeys 找到 key 的位置
2. 用下标去 mValues 取值

结构示意:

rust 复制代码
┌──────────────┐     ┌──────────────┐
│ mKeys  int[] │     │ mValues Obj[]│
├─────┬────────┤     ├─────┬────────┤
│  0  │   1    │ --> │  0  │  "A"   │
│  1  │   3    │ --> │  1  │  "B"   │
│  2  │   5    │ --> │  2  │  "C"   │
└─────┴────────┘     └─────┴────────┘

删除元素时不会立刻移动数组 ,而是标记为"DELETED"占位符,等需要时再 gc() 进行压缩

二分查找大家都懂吧,就是那个过程很简单,细节是魔鬼的算法,简单介绍下,在给定一个有序的数据结构中去查找一个元素,通过目标元素的特征与中间的元素比较确定它属于哪部分,往复循环,每次都是缩小目标数据结构的一半,直到找到元素,很好理解是不是,恩,保持你对它的第一印象就行了。

原理

SparseArray 内部使用两个并行的数组:一个用于存储键(int[] mKeys),另一个用于存储值(Object[] mValues)。当添加一个键值对时,它会根据键的大小将键插入到 mKeys 数组的正确位置,并同时将值插入到 mValues 数组的对应位置。由于数组是连续存储的,查找操作使用二分查找(binary search),其时间复杂度为 O(logn),而添加和删除操作可能需要移动数组元素,其时间复杂度为 O(n)。

作用与功能

SparseArray 避免了 HashMap 中每个键值对都需要创建一个 Entry 对象的开销。在 HashMap 中,每个 Entry 对象包含了键、值以及指向下一个 Entry 的引用,这些都会占用额外的内存。而 SparseArray 仅仅存储原始的 int 键和 Object 值,因此内存消耗更低。

Java 泛型不支持基本类型,Integer 会频繁装箱(int → Integer)和拆箱(Integer → int),增加 CPU 与内存占用。SparseArray就避免了频繁装箱/拆箱。

虽然添加和删除操作可能较慢,但对于查找操作,二分查找在数据量不大的情况下(通常在数百或数千个元素以内),其性能可以媲美甚至超过 HashMap。数据量大时,性能退化至少 50 %!

SparseArray 家族

SparseArray 只能存储 key 为 int 的数据,当我们要存储的数据 key 不为 int 类型或 value 是基本类型 怎么办?不慌,下面有请SparseArray 家族成员登场

  • SparseArray<E>
    Key = int,Value = Object ,替代 HashMap<Integer, E>
  • SparseBooleanArray
    Key = int,Value = boolean ,替代 HashMap<Integer, Boolean>
  • SparseIntArray
    Key = int,Value = int ,替代 HashMap<Integer, Integer>
  • SparseLongArray
    Key = int,Value = long ,替代 HashMap<Integer, Long>
  • LongSparseArray<E>
    Key = long,Value = Object ,替代 HashMap<Long, E>

应用场景

  • 键为 int 类型:当你的键是整数时,这是首选。
  • 数据量相对较小 :如果键值对的数量不多,SparseArray 的性能优势更为明显。
  • 键是稀疏的 :即使键的数值范围很大,但实际使用的键数量不多,SparseArray 也能很好地工作。例如,你可能需要将视图 ID(如 R.id.my_view)映射到某个对象,这些 ID 是不连续的,但总数不多。

在安卓中常见的就是存储监听/订阅列表或存储 View 之类的。

缺点

  • 插入时会用二分查找 + 数组移动,数据量大时效率比 HashMap 低
  • 不是线程安全的
  • 删除操作延迟清理(gc() 机制)

如果只使用SparseArray 你就会发现,还是 HashMap 好用,因为SparseArray 没有像HashMap 那种以对象为 key 的存储方式,不要慌,接下来登场的是另一个安卓特有集合《ArrayMap

二、ArrayMap

ArrayMap 是一种通用型的键值对数据结构,它的设计目的是为了在键值对数量较少时,替代 HashMap 以获得更高的内存效率和更好的性能。

特点 :一个 int[] mHashes(存 hashCode,升序),一个 Object[] mArray(交错存储 key 和 value)

vbnet 复制代码
mHashes: [100, 200, 350]
mArray:  [ "key1", "val1", "key2", "val2", "key3", "val3" ]

结构示意:

rust 复制代码
┌────────────────┐      ┌────────────────────────────────┐
│ mHashes  int[] │      │ mArray Object[]                │
├─────┬──────────┤      ├─────┬─────────────┬─────┬──────┤
│  0  │  100     │ -->  │  0  │  "key1"     │  1  │"val1"│
│  1  │  200     │ -->  │  2  │  "key2"     │  3  │"val2"│
│  2  │  350     │ -->  │  4  │  "key3"     │  5  │"val3"│
└─────┴──────────┘      └─────┴─────────────┴─────┴──────┘

查找流程:

  1. mHashes 用二分查找 hashCode
  2. mArray 中取出对应的 key,调用 equals() 比较
  3. 如果 key 相等,返回对应的 value

查找下标的操作:

arduino 复制代码
int indexOf(Object key, int hash) {
  	// 当前数据的大小
    final int N = mSize;

    // Important fast case: if nothing is in here, nothing to look for.
  	// 如果大小为0,说明没有数据存在,返回 -1 表示插入
    if (N == 0) {
        return ~0;
    }
		
  	// 二分查找到下标
    int index = binarySearchHashes(mHashes, N, hash);

    // If the hash code wasn't found, then we have no entry for this key.
  	// 返回要插入的下标位置
    if (index < 0) {
        return index;
    }

    // If the key at the returned index matches, that's what we want.
    // 如果返回索引处的键匹配,那就是我们想要的。index 是 0 的话,那么它的值就是1的位置。
    // index<<1 就等于 0 + 1
    if (key.equals(mArray[index<<1])) {
        return index;
    }

    // Search for a matching key after the index.
  	// 遇到冲突,开放地址发解决冲突,从前往后找
    int end;
    for (end = index + 1; end < N && mHashes[end] == hash; end++) {
        if (key.equals(mArray[end << 1])) return end;
    }

    // Search for a matching key before the index.
  	// 从后往前找
    for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
        if (key.equals(mArray[i << 1])) return i;
    }

    // Key not found -- return negative value indicating where a
    // new entry for this key should go.  We use the end of the
    // hash chain to reduce the number of array entries that will
    // need to be copied when inserting.
 		// 未找到是取反表示需要插入的位置
    return ~end;
}

原理

SparseArray 类似,ArrayMap 也是通过两个并行的数组来实现的:一个用于存储键的哈希码(int[] mHashes),另一个用于存储键值对(Object[] mArray)。mArray 数组中,键和值是交替存储的,即 mArray = {key1, value1, key2, value2, ...}。查找、添加和删除操作的实现也类似于 SparseArray,首先通过二分查找找到键的哈希码,然后检查键是否相等。

作用与功能

ArrayMap 同样避免了为每个键值对创建 Entry 对象的开销,从而大大减少了内存占用。对于少量键值对,ArrayMap 的内存消耗远低于 HashMap

在数据量较小(通常在数百个元素以内)时,ArrayMap 的查找、添加和删除操作由于其精简的内部结构和对哈希冲突的优化处理,其性能可以优于 HashMap。它不需要处理复杂的链表或红黑树结构,代码实现更轻量。

应用场景

  • 键值对数量较少ArrayMap 的性能拐点通常在数百个元素左右,当数据量超过这个范围,HashMap 的性能优势会逐渐显现。
  • 需要通用键值对存储 :与 SparseArray 不同,ArrayMap 的键可以是任意对象,而不仅仅是 int 类型。
  • 性能和内存都是关键考虑因素 :在内存受限的移动设备上,例如在一个 RecyclerViewAdapter 中存储少量的数据,ArrayMap 是一个很好的选择。

总的来说 ArrayMap 就是 替代 HashMap<K,V> 的轻量实现。

缺点

  • 插入/删除需要移动数组,数据量大时性能差
  • 不是线程安全的
  • 数据量大时性能不如 HashMap

三、总结

  • SparseArray 系列 :Key 是原始数值类型(int、long),替代 HashMap<Integer, T>,避免装箱节省内存。
  • ArrayMap :Key 可以是任意对象,替代 HashMap<K,V>,小数据量节省内存。
  • 共同点
    • 数据量少时比 HashMap 更省内存
    • 查找是二分法 + 数组结构
    • 插入删除需要移动数据,性能不适合超大数据集
相关推荐
快去睡觉~1 小时前
力扣238:除自身之外数组的乘积
数据结构·算法·leetcode
Aczone281 小时前
数据结构(五):顺序循环队列与哈希表
数据结构·哈希算法·散列表
yics.1 小时前
数据结构——栈和队列
java·数据结构
whysqwhw1 小时前
安卓内存优化
android
用户2018792831672 小时前
TabLayout禁止滑动 + 左对齐排列实现
android
whysqwhw2 小时前
安卓Drawable分类
android
秋难降3 小时前
【数据结构与算法】———回溯之美
数据结构·算法
ikkkkkkkl3 小时前
LeetCode:347.前K个高频元素
数据结构·c++·算法·leetcode
筱砚.3 小时前
【数据结构——并查集】
数据结构·算法