Android中推荐用SparseArray替换HashMap<Integer,Object>

本文分析了Android中SparseArray与HashMap的区别,强调SparseArray在内存效率上的优势,避免了自动装箱和额外对象的开销。通过源码解析,详细介绍了SparseArray的构造、put、get和remove等方法,以及其内存性能优化策略,如二分查找和gc清理。建议在适合的场景下使用SparseArray以提升性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文借鉴自文章Android中你还在用HashMap<Integer,Object>吗?

在Android Studio中输入以下代码:

Map<Integer,Object> map = new HashMap<>();

系统会提示:

Use new SparseArray<Object>(...) instead for better performance

推荐我们使用SparseArray&lt;Object>来取代HashMap&lt;Integer,Object>,以获取更好的性能。

SparseArray简介

SparseArrayjava.util包下的类,介绍如下:

SparseArrays map integers to Objects. Unlike a normal array of Objects, there can be gaps in the indices. It is intended to be more memory efficient than using a HashMap to map Integers to Objects, both because it avoids auto-boxing keys and its data structure doesn’t rely on an extra entry object for each mapping.

大致意思是:

SparseArrays是个整型到对象的映射,与一般的对象数组不同,它的索引值可以为空。它设计的目的是为了比HashMap<Integer,Object>拥有更高的内存性能。
主要优点有两个:

1. 它避免了对key自动装箱的操作;
2. 它的映射关系并不依赖于额外的对象;

简单提一下自动装箱:Integer i = 6; 将int基本数据类型的值包装成Integer类的对象,底层实际上调用Integer的构造函数,将int类型的值作为参数传入。

可能有人会有疑惑:为什么HashMap中的key是Integer类型而不是int类型,我们平常写入的基本上都是int类型的,这样还要自动装箱不是浪费吗?

这要从HashMap的原理来看,HashMap是通过key的hashcode值初步定位,而基本数据类型由于只能存放在栈空间中,没有对象可言,因而也没有hashCode()等方法,所以需要自动装箱成Integer类型,调用Integer类的hashCode()方法来获取hashcode值,这样不可避免地造成了性能的浪费。

这就对SparseArray提出了考验,怎样能够避免自动装箱造成的浪费同时又能够读写映射关系呢?

SparseArray的使用

我们先来对比一下SparseArray和HashMap的使用,代码如下:

//定义
SparseArray<Object> sparseArray = new SparseArray<>() ;
Map<Integer,Object> map = new HashMap<>() ;
//添加数据
sparseArray.put(1,new Object());
map.put(1,new Object()) ;
//取数据
Object o1 = sparseArray.get(1) ;
Object o2 = map.get(1) ;
//删除数据
sparseArray.remove(1);
Object o3 = map.remove(1);

可以看出两者在使用上没有什么区别,只是SparseArray的remove方法没有返回值,完全可以类比HashMap的用法去使用SparseArray。

从源码分析SparseArray的内存性能

成员变量
//存储索引集合
private int[] mKeys;

//存储对象集合
private Object[] mValues;

//存储键值对总数
private int mSize;

//已删除值通用的标记对象
private static final Object DELETED = new Object();

//是否需要进行gc
private boolean mGarbage = false;

和HashMap的数据结构不同,HashMap是通过数组+链表的数据结构存储键值对,而SparseArray是创建两个数组存储。

由于采用int类型的数组,避免了自动装箱的操作,同时key和value之间不需要有额外的对象维护映射,后文可以看出是采用数组间一一对应的方式维护的,所以内存的使用效率很高。

构造函数
public SparseArray() {
    this(10);
}

public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        // 返回零长度的数组或者集合,而不是null
        mKeys = ContainerHelpers.EMPTY_INTS;
        mValues = ContainerHelpers.EMPTY_OBJECTS;
    } else {
        mKeys = new int[initialCapacity];
        mValues = new Object[initialCapacity];
    }
    mSize = 0;
}
put
public void put(int key, E value) {

    //此处调用了ContainerHelpers类的binarySearch方法,后面会介绍
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    //如果i符合条件则替换i处的value值
    if (i >= 0) {
        mValues[i] = value;
    } else {

        //取反,用于标定插入的位置,后文会介绍
        i = ~i;

        //特殊情况,空间未满的情况下i所在处的value值为空,直接插入
        if (i < mSize && mValues[i] == DELETED) {
            mKeys[i] = key;
            mValues[i] = value;
            return;
        }

        //空间已满,调用gc回收垃圾
        if (mGarbage && mSize >= mKeys.length) {
            gc();

            // Search again because indices may have changed.
            i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
        }

        mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
        mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
        mSize++;
    }
}

ContainerHelpers类提供的二分查找法是put方法的精髓,其实现如下:

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
    }

就是一个标准的二分查找,“>>>”右移符号为了避免溢出,如果未匹配上返回的是lo的取反值,这样返回值为非负则查找成功,为负则查找失败,put方法中就可以通过返回值来做相应的操作。

put方法中else后面紧跟的取反操作可以将返回的负值再变回原值,而原值的lo代表当前元素按照升序排列应该插入的下标,经过取反操作后可以直接插入。

put方法中最后的insert方法会在数组已满的时候自动扩容,扩容代码如下:

public static <T> T[] insert(T[] array, int currentSize, int index, T element) {
    assert currentSize <= array.length;

    if (currentSize + 1 <= array.length) {
        System.arraycopy(array, index, array, index + 1, currentSize - index);
        array[index] = element;
        return array;
    }

    //下面的是容量不够的情况
    @SuppressWarnings("unchecked")
    T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
            growSize(currentSize)); 
    System.arraycopy(array, 0, newArray, 0, index);
    newArray[index] = element;
    System.arraycopy(array, index, newArray, index + 1, array.length - index);
    return newArray;
}

System.arraycopy方法用于拷贝数组,如果操作的数组对象相同,则复制过程就是将数组中的要移动的部分复制到一个临时数组中,然后再从这个临时数组中将数据复制到原数组中,相当于整体移位。

扩容的算法是由growSize方法控制的,源码如下:

public static int growSize(int currentSize) {
    return currentSize <= 4 ? 8 : currentSize * 2;
}
get

get方法最终调用下面这个方法,其中valueIfKeyNotFound的值为null

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];
    }
}

这个方法简单,二分法查找,如果没找到返回空,找到了返回值。

remove

remove方法最终调用的是delete方法,代码如下:

public void delete(int key) {
    int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

    if (i >= 0) {
        if (mValues[i] != DELETED) {
            mValues[i] = DELETED;
            mGarbage = true;
        }
    }
}

这里的设计也考虑了性能优化,二分法找到需要删除的value后,并没有立即删除,而是用DELETE做了标记,并且将可清理状态设置为true,等待重新插入值或者是gc清理。

gc
private void gc() {
    // Log.e("SparseArray", "gc start with " + mSize);

    int n = mSize;
    int o = 0;
    int[] keys = mKeys;
    Object[] values = mValues;

    for (int i = 0; i < n; i++) {
        Object val = values[i];

        if (val != DELETED) {
            if (i != o) {
                keys[o] = keys[i];
                values[o] = val;
                values[i] = null;
            }

            o++;
        }
    }

    mGarbage = false;
    mSize = o;

}

gc的原理就是遍历数组,将非DELETE标记的资源往前移。

总结

  1. 使用上SparseArrayMap&lt;Integer,Object>没有多大区别,只是remove方法前者没有返回值。
  2. SparseArrayMap&lt;Integer,Object>消耗的内存少,避免了自动装箱和创建维持映射所需要的对象。
  3. SparseArray采用二分法查找,时间复杂度为O(logn),HashMap的key需要进行hash运算,在没有冲突的情况下是O(1),冲突的情况下是O(n),但是SparseArray插入数据时涉及到对象的移动,而HashMap不需要,所以速度上前者稍慢,选择使要根据情况而定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值