SparseArray到底哪点比HashMap好

本文深入剖析了Android中的SparseArray类,对比HashMap,详细介绍了SparseArray的数据结构、关键方法如put、get、delete等,并分析了其高效性和内存节省的原因。

SparseArray是android里为<Interger,Object>这样的Hashmap而专门写的class,目的是提高效率,其核心是折半查找函数(binarySearch)。

HashMap底层是一个Hash表,是数组和链表的集合实现,有需要的可以去看看我关于Hashmap的分析。hashmap源码分析

所以Android开发中官方推荐:当使用HashMap(K, V),如果K为整数类型时,使用SparseArray的效率更高。

那我们看源码来分析下,

构造函数:

/**
 * 存储索引集合.
 */
private int[] mKeys;
/**
 * 存储对象集合.
 */
private Object[] mValues;
/**
 * 存储的键值对总数.
 */
private int mSize;
/**
 * 采用默认的构造函数,则初始容量为10.
 */
public SparseArray() {
    this(10);
}
/**
 * 使用指定的初始容量构造SparseArray.
 *
 * @param initialCapacity 初始容量
 */
public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        // Effective Java中第43条:返回零长度的数组或者集合,而不是:null
        mKeys = ContainerHelpers.EMPTY_INTS;
        mValues = ContainerHelpers.EMPTY_OBJECTS;
    } else {
        // 构造initialCapacity大小的int数组和object数组
        mKeys = new int[initialCapacity];
        mValues = new Object[initialCapacity];
    }
    // 设置SparseArray存储的<key,value>键值对个数为0.
    mSize = 0;
}
和HashMap的数据结构不同,HashMap是使用 数组+链表 的数据结构存储键值对,而SparseArray只是用了 两个数组 进行存储。

我们知道链表的时间复杂度是很高的,这估计也是造成hashmap时间复杂度高的一个原因。

ContainerHelpers

ContainerHelpers类提供了二分查找算法,这也一定程度上提高了查找的效率

<span style="font-size:12px;">class ContainerHelpers {
    // This is Arrays.binarySearch(), but doesn't do any argument validation.
    static int binarySearch(int[] array, int size, int value) {
        // 获取二分的起始和结束下标.
        int lo = 0;
        int hi = size - 1;
        while (lo <= hi) {
            // 获取中点的下标和值
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];
            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        return ~lo;  // value not present
    }
}</span>

put()函数

/**
 * 在SparseArray中存储键值对.
 */
public void put(int key, E value) {
    // 通过二分查找算法计算索引
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i >= 0) {
        // key已经存在对应的value,则直接替换value.
        mValues[i] = value;
    } else {
        i = ~i;
        if (i < mSize && mValues[i] == DELETED) {
            // 特殊的case,直接存储key-value即可
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }
        if (mGarbage && mSize >= mKeys.length) {
            // 如果有元素被删除,并且目前容量不足,先进行一次gc
            gc();
            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }
        // 扩容
        if (mSize >= mKeys.length) {
            // 获取扩容的数组大小
            int n = mSize + 1;
            int[] nkeys = new int[n];
            Object[] nvalues = new Object[n];
            // 数组拷贝最好使用System.arraycopy,而不是自己重撸一遍
            System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
            System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
            mKeys = nkeys;
            mValues = nvalues;
        }
        // i为插入位置,如果i<mSize,则i之后的元素需要依次向后移动一位.
        if (mSize - i != 0) {
            System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
            System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
        }
        // 设置值,存储数量+1
        mKeys[i] = key;
        mValues[i] = value;
        mSize++;
    }
}

put函数的逻辑:

  1. 通过二分查找算法,计算key的索引值.
  2. 如果索引值大于0,说明有key对应的value存在,直接替换value即可.
  3. 如果索引值小于0,对索引值取反,获取key应该插入的坐标i.
  4. 判断是否需要扩容:1.需要扩容,则先扩容; 2.不需要扩容,则利用System.arraycopy移动相应的元素,进行(key,value)键值对插入.

get()函数

get函数就是利用二分查找获取key的下标,然后从object[] value数组中根据下标获取值. 
之所以SparseArray号称比HashMap有更好的性能:

  1. SparseArray更加节约内存,一个int[]数组存储所有的key,一个object[] 数组存储所有的value.
  2. HashMap遇到冲突时,时间复杂度为O(n).而SparseArray不会有冲突,采用二分搜索算法,时间复杂度为O(lgn).
/**
 * 根据指定的key获取value.
 */
public E get(int key) {
    return get(key, null);
}
/**
 * 利用二分查找算法根据key获取指定的value.
 */
public E get(int key, E valueIfKeyNotFound) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
    if (i < 0 || mValues[i] == DELETED) {
        return valueIfKeyNotFound;
    } else {
        return (E) mValues[i];
    }
}

delete()函数

/**
 * 根据key删除指定的value.
 */
public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        if (mValues[i] != DELETED) {
            // 标记i的值为private static final Object DELETED = new Object();
            mValues[i] = DELETED;
            // 设置gc标记为true.
            mGarbage = true;
        }
    }
}
/**
 * Alias for {@link #delete(int)}.
 */
public void remove(int key) {
    delete(key);
}

gc()函数

if (mGarbage && mSize >= mKeys.length) {
    // 如果有元素被删除,并且目前容量不足,先进行一次gc
    gc();
    // Search again because indices may have changed.
    i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
}
通过上面的源码分析,我们不难得出:
正式因为SparseArray采用了数组这种形式,才使得我们的key在做hash运算的时候,通过二分查找时间复杂度降低了,从而提高了效率。
通过二分查找保证查询效率为O(lgn).而HashMap在未冲突的情况下是O(1),冲突的情况下是O(n).





SparseArray 是 Android 提供的一种专为映射 `int` 到 `Object` 而设计的数据结构,其内部通过两个独立的数组分别存储键(`int` 类型)和值(`Object` 类型),并且通过二分查找算法来提高查找效率[^4]。这种实现方式避免了 `HashMap` 中由于自动装箱导致的额外开销,并且在特定场景下比 `HashMap` 更加高效。 ### 数据结构 SparseArray 使用两个数组分别存储键和值: - `mKeys[]`:用于存储键的 `int` 数组。 - `mValues[]`:用于存储值的 `Object` 数组。 这两个数组的长度始终保持一致,且每个键值对在两个数组中的索引位置相同。SparseArray 内部采用二分查找算法进行键的查找,使得查找的时间复杂度为 `O(log n)`,而插入和删除操作则需要移动数组元素,时间复杂度为 `O(n)`[^4]。 当初始化 SparseArray 时,可以通过指定初始容量,否则默认容量为 10。实际分配的数组大小会通过 `ArrayUtils.idealIntArraySize()` 方法进行优化,以适应内存对齐策略[^3]。 ### 添加删除操作 SparseArray 提供了两种添加元素的方式: - `put(int key, E value)`:根据键插入或更新值。 - `append(int key, E value)`:直接在数组末尾追加键值对,适用于键值递增的场景,效率高于 `put` 方法。 删除操作包括: - `delete(int key)`:标记删除指定键的条目。 - `remove(int key)`:功能 `delete` 相同,但内部实现上略有不同。 值得注意的是,SparseArray 在删除元素时并不会立即压缩数组,而是将对应位置标记为空,后续插入操作可能会复用这些空位,从而减少数组扩容的频率[^2]。 ### 遍历查找 SparseArray 提供了以下方式遍历数据: - 通过 `size()` 方法获取当前元素数量,结合 `keyAt(int index)` 和 `valueAt(int index)` 进行遍历。 - 可以直接使用 `get(int key)` 获取指定键的值。 查找操作通过二分法实现,时间效率为 `O(log n)`,适用于数据量较小的场景。 ### 使用场景 SparseArray 主要适用于以下场景: - 数据量较小(通常在几百以内),稀疏分布的键值映射。 - 键为 `int` 类型,避免使用 `Integer` 装箱带来的性能开销。 - 对查找效率有一定要求,但插入和删除操作相对较少。 - 在 Android 系统中,SparseArray 被广泛用于 UI 组件、事件映射等场景,例如 `RecyclerView` 中的 ViewHolder 缓存管理、事件监听器的注册等。 ### 示例代码 ```java SparseArray<String> sparseArray = new SparseArray<>(); // 添加元素 sparseArray.put(1, "Value1"); sparseArray.append(2, "Value2"); // 修改元素 sparseArray.put(1, "NewValue"); // 删除元素 sparseArray.delete(2); // 遍历元素 for (int i = 0; i < sparseArray.size(); i++) { int key = sparseArray.keyAt(i); String value = sparseArray.get(key); Log.d("SparseArrayExample", "Key: " + key + ", Value: " + value); } ``` ### 优缺点分析 **优点:** - 避免了 `HashMap` 中 `Integer` 装箱的开销。 - 使用二分查找提升查找效率。 - 对内存使用更友好,尤其在数据量较小时。 **缺点:** - 插入和删除操作效率较低,尤其是数据量较大时。 - 不适合处理大规模数据集。 - 不支持并发访问,需手动加锁。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值