关闭

SparseArray

632人阅读 评论(0) 收藏 举报
分类:

SparseArray

基础

        Java的HashMap使用的是数组+链表的实现结构,无论有没有是否添加数据,都会分配一个数组(该数组用于存放链表的头元素)。这在某种程度上浪费了一部分的内存空间,而且HashMap的key,value必须是Object,所以对于基本数据类型来说,在存储到HashMap时必须经过一个自动装箱的过程,这又浪费了一部分性能。这算是HashMap的缺点。

        对于SparseArray来说,它避免了HashMap的自动装拆箱的过程:因为它的key值是int类型的。它内部采用双数组实现:一个int[]类型数组,用于存储key值;一个Object[]数组,用于存储value。这里就有一个与ArrayList类似的问题:数组扩容——当初始容量不够,需要扩展一下key数组,value数组。

        因此,在SparseArray的使用时,应该考虑初始容量的大小。大了,浪费内存;小了,数组扩容频繁,导致性能降低。

构造函数

    public SparseArray() {
        this(10);
    }

    public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;
    }

        很简单,初始化两个数组,并将mSize设置为0。mSize指的是当前数组中具体存有的元素的个数,此处不能用数组的length——它只数组能存储的元素的个数,并不代表着数组中已存储了多少个元素。

get

如下:

    public E get(int key) {
        return get(key, null);
    }

    @SuppressWarnings("unchecked")
    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];
        }
    }

        ContainerHelpers.binarySearch()使用的是二分查找。为什么可以直接用二分查找,是因为mKeys数组中是已经排好序的,在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
    }

        这个方法有一点要注意,如果没有找到指定value时的返回值:~lo(取反),而整个代码中可以看出:lo是不可能小于0的,所以~lo是小于0的。

        因此,binarySearch()的返回值有两个作用:大于0,则代表所查找value在array中的下标;小于0,则代表array中没有该value,并且此时的返回值取反后为value应该插入到array中的下标。一个返回值两个作用:太帅了!!

        知道这点后,就可以看出get的逻辑,其中==DELETED的逻辑在remove时说。其余略。

        但,这里的无参get写的不太好,因为SparseArray本身的value可以是null。如果当前没有该key,并且调用了无参数的get方法,返回的也是null,容易造成模糊。

remove

如下:

    /**
     * Removes the mapping from the specified key, if there was any.
     */
    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

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

    /**
     * @hide
     * Removes the mapping from the specified key, if there was any, returning the old value.
     */
    public E removeReturnOld(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                final E old = (E) mValues[i];
                mValues[i] = DELETED;
                mGarbage = true;
                return old;
            }
        }
        return null;
    }

    /**
     * Alias for {@link #delete(int)}.
     */
    public void remove(int key) {
        delete(key);
    }
        逻辑很简单,并且remove与delete可以认为是同一个方法。removeReturnOld时,将旧有的值给返回了。

        在remove时,并不是将当前key对应的value设置为null,而是设置成DELETED,它的定义如下:

    private static final Object DELETED = new Object();

        这样做的目的有两个:其一不用remove/delete时直接移动数组,因为内部采用的是双数组结构,对于数组来说查找方便,但插入删除很耗性能,所以并没有在每一次删除时都移动数组。这里使用DELETED相当于占位符,不用移数组,而且也知道该位置已经被remove/delete掉了,get时就能知道返回何值。其二,并没有直接设置为null,这样保证了SparseArray中可以存储null值。

gc

        这是一个private方法,但这个方法很有意思,而且对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;
    }

        该方法的功能很简单:移除掉那些被remove/delete过的key。为什么要这么做?为了节约内存。为什么不直接在remove/delete时删掉,原因见remove部分。

put

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

        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;

            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }

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

        先查找key的位置,如果当前key已经存在,直接覆盖对应的value。如果没有,但要插入的位置上的元素为DELETED,就直接用新key,value取代原有的。否则就直接使用insert进行插入。

        这中间有一个判断,当remove/delete(mGarbage只有在remove时才会被赋值为true)过,并且数组已经存储满了,那么就调用gc删除其中的deleted,然后再插入——这是为了在尽可能不影响性能的情况下节约内存。如果mGarbage为false,表示没有remove过,那么就不需要调用gc()。如果mSize<mKeys.length表明当前的数组并没有存满,现在不需要移动deleted。如果两个条件都满足,就表示当前数组中有脏数据,gc()之后就会空余出一部分空间,就不需要重新扩容了。

        在put时也涉及到数组的扩容,即思路与ArrayList中完全一样。代码如下:

public static <T> T[] More ...insert(T[] array, int currentSize, int index, T element) {
        assert currentSize <= array.length;  //断言,当前的size肯定要小于数组的长度
        if (currentSize + 1 <= array.length) {//数组容量足够,不需要扩容,只是将index后面的往后挪一位            
            System.arraycopy(array, index, array, index + 1, currentSize - index);
            array[index] = element;
            return array;
        }
        @SuppressWarnings("unchecked")
        //growSize代码为currentSize <= 4 ? 8 : currentSize * 2;
        T[] newArray = ArrayUtils.newUnpaddedArray((Class<T>)array.getClass().getComponentType(),
                growSize(currentSize));//生成一个新数组,容易为growSize。这里就是进行了扩容,逻辑完全与ArrayList相同
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
}

其余

        后面还有一些方法,比较简单,捡其中两个说下:

        clear():清空value数组,将其中的value全部设置为null。

        append():类似于put,但比put的要求要多一个:使用append的前提时你知道你的key是比SparseArray中所有的key都大的。因为二分查找时要求Key时从小到大排列的。在满足这一点的前提下,使用append比put效率高。

总结

        1,采用双数组结构可以模拟Map。扩展一下,采用多个数组,就可以模拟一对多的情况:一个数组值key,多个数组存value。当然,也可将多个value组建成珍上Object,直接使用SparseArray。

        2,善用函数返回值。一个返回值可以表达多种信息。

        3,将可能执行延误性能的操作尽量延迟到必须执行时才进行,因为有可能不执行。如上面的并不有在remove时直接移动数组,而是用DELETED做标识,到容量不够时才gc。

      












        

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:102643次
    • 积分:2738
    • 等级:
    • 排名:第13638名
    • 原创:173篇
    • 转载:0篇
    • 译文:0篇
    • 评论:1条
    最新评论
  • MVP入门

    huihuang2: 博主,写的深入浅出 ,通俗易懂,实在是一篇不错的博文