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

来源:互联网 发布:罗马全面战争火攻算法 编辑:程序博客网 时间:2024/05/21 14:42

本文借鉴自文章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();//是否需要进行gcprivate 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不需要,所以速度上前者稍慢,选择使要根据情况而定。
阅读全文
0 0
原创粉丝点击