Android特别的数据结构(二)ArrayMap源码解析

ArrayMap是Android中的一个高效数据结构,由mHashes和mArrays两个数组组成。插入元素时,根据Key的hash值找到合适的位置,处理哈希冲突。删除元素时,可能触发减容操作。ArrayMap相比于HashMap内存占用更小,适用于Key为任意类型的场景。

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

1. 数据结构

public final class ArrayMap<K,V> implements Map<K,V>

  • 由两个数组组成,一个int[] mHashes用来存放Key的hash值,一个Object[] mArrays用来连续存放成对的Key和Value
  • mHashes数组按非严格升序排列
  • 初始默认容量为0
  • 减容:
    • mHashes的长度就表征了 ArrayMap 当前的容量 ,而 mSize 则表征当前元素个数
    • 删除元素后如果当前mHashes长度比 BASE_SIZE*2 大,但总元素个数又没到当前容量的 1/3 就减容
  • 扩容:
    • 插入元素时,如果需要扩容:
    • 如果当前长度大于8,就按1.5倍扩容
    • 如果当前长度大于4,小于8,就扩容到8
    • 如果当前长度小于4,扩容到4
  • 由于使用了hash,可能存在哈希冲突,结局方式:将相同的hash的key连续存放
  • mHashes的index和Key、Value的下标对应关系
    • key = mArrays[index*2] –> key = mArrays[index << 1]
    • value = mArrays[index*2 +1] --> value = mArrays[index<<1+1]

请添加图片描述

public final class ArrayMap<K, V> implements Map<K, V> {
    @UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use public key/value API.
    int[] mHashes;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Storage is an implementation detail. Use public key/value API.
    Object[] mArray;
    @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
    int mSize;
    //扩容的判断条件
    private static final int BASE_SIZE = 4;
    
}

2. 插入

  • public V put(K key, V value)

ArrayMap和HashMap一样,允许Key为null,且其hash默认置为0,且可以通过判空或者equals()来进行Hash冲突时,对目标Key的判断。在插入时,主要做了:

  1. 找到Key所在的mHashes下标
  2. 如果之前已经添加过,直接覆盖value
  3. 如果之前没有添加过,插入元素(可能需要先扩容)
@Override
public V put(K key, V value) {
    final int osize = mSize;
    final int hash;
    int index;
    if (key == null) {
        //key=null的时候认为hash=0,HashMap也是这样设计的!
        hash = 0;
        //根据key=null去寻找合适的index
        //----------解释(1)----------
        index = indexOfNull();
    } else {
        //如果key不为空,就可以获取一个hash值,可以用系统的方法,也可以用这个对象本身的hashcode()
        hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
        //由于可能用到对象本身的hashcode()这久无法避免出现hash冲突
        //通过hash和key来查找合适的下标。为了避免hash冲突,还把key放进来了,可以通过equals()来处理hash冲突
        //----------解释(2)----------
        index = indexOf(key, hash);
    }
    //假设index找到了,是个正数,说明之前已经添加过这个key的元素了,直接对value进行替换!
    if (index >= 0) {
        //放在index/2+1的位置
        index = (index << 1) + 1;
        //把旧数据返回出去
        final V old = (V) mArray[index];
        //新数据填入
        mArray[index] = value;
        return old;
    }
    //如果之前没有添加过这个Key的元素,这下要添加,就要移动数组后续部分
    //index又取反回来,得到了之前找的最后一步end所在的位置,也就是需要插入的位置
    index = ~index;
    //如果需要扩容
    if (osize >= mHashes.length) {
        //1. 如果旧size超过了 base_size*2,那么新的大小为 osize * 1.5;
        //2. 如果还没超过 base_size*2:
        //2.1 如果超过了 base_size, 那么新大小直接定为 base_size * 2;
        //2.2 否则,新大小直接定为 base_size
        final int n =
            osize >= (BASE_SIZE * 2) ?
            (osize + (osize >> 1)) : (osize >= BASE_SIZE ? (BASE_SIZE * 2) : BASE_SIZE);

        final int[] ohashes = mHashes;//原来的hash数组
        final Object[] oarray = mArray;//原来的元素实体数组
        //根据 n 去申请一个新的数组
        //----------解释(3)----------
        allocArrays(n);

        if (mHashes.length > 0) {
            //把原来的值赋值填充到新的数组上去
            System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
            System.arraycopy(oarray, 0, mArray, 0, oarray.length);
        }
        //“起死回生”,尝试将无用的原有数组用作缓存,如果不能用作缓存,就直接释放不管了
        //----------解释(3)----------
        freeArrays(ohashes, oarray, osize);
    }
    //判断是否需要扩容,并完成需要的扩容之后,开始插入元素

    //1. 平移一下,让出位置
    if (index < osize) {
        if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize - index)
                         + " to " + (index + 1));
        //元素的hash是一个接一个的,所以只需要挪动一个位置即可
        System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
        //但是元素本身存储,需要连着存储其key和value,占用两个数组单元,
        // 所以挪动的时候,需要将hash个数对应两倍的个数进行挪动
        System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
    }

    if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
        if (osize != mSize || index >= mHashes.length) {
            throw new ConcurrentModificationException();
        }
    }
    //2. 插入数据
    //可以看出来,index*2和 index*2+1连续两个位置,存放着key和value!
    // 也就是不用额外造一个对象,而是直接两个数据一起填入
    //mArray放的是数据[key0,value0,key1,value1,...,keyN,valueN,...]
    //mHashes则是在对应index存放key的hash值
    mHashes[index] = hash;
    mArray[index << 1] = key;
    mArray[(index << 1) + 1] = value;
    //更新mSize
    mSize++;
    //由于并没有把谁“覆盖掉”,所以不需要返回oldValue,直接返回null即可
    return null;
}

在此之前,认为读者已经熟知SparseArray的源码,了解其中的按位取反~设计、二分查找设计的思想。来看一下插入的大概流程:

请添加图片描述

解释(1)

  • indexOfNull()

可以看到,如果要插入的Key为null,需要在mHashes中查找hash=0的元素位置。除了null的hash=0,其他元素的hash也有可能为0,所以需要进行hash冲突情况下的key查找:

int indexOfNull() {
    final int N = mSize;
    if (N == 0) {
        //~~0可以得到接下来可以插入的位置(为0)
        return ~0;
    }
    //先找hash=0是否存在
    int index = binarySearchHashes(mHashes, N, 0);
	//如果之前都没有插入过hash=0的元素,直接返回一个负数
    //~index可以得到接下来可以插入的位置
    if (index < 0) {
        return index;
    }

    //如果存在hash=0,那么就找到对应的Key,如果是null,就对了
    if (null == mArray[index << 1]) {
        return index;
    }

    //如果存在hash=0,但是对应的key不是null,说明存在hash冲突
    int end;
    //index为Key的hash在mHashes[]中的下标
    //end<<1 = end*2 即定位到mArrays中Key的位置
    //由于相同的hash在mHashes[]中是连续存放的,所以只需要向左向右遍历判断Key是否一致即可
    //从index处的key开始向右遍历判断
    for (end = index + 1; end < N && mHashes[end] == 0; end++) {
        //如果找到了一个Key为null,就返回这个index
        if (null == mArray[end << 1]) return end;
    }

    //如果找到结尾了都没找到,就往前找
    for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
        if (null == mArray[i << 1]) return i;
    }

    //如果整个mArrays[]都没有null这个key,就返回一个负数表示没有找到
    return ~end;
}

需要注意的是,二分查找找到一个hash,它不一定是一组连续相同hash的第一个index。

例如我们要查的Key的hash=10,相同的Key应当在mArrays中下标为2位置:

请添加图片描述

首先根据Key的hash值来快速定位Key的大致位置

  • 二分发现值为10的hash在mHashes数组中下标index为2,其对应的Key的下标为index*2也就是4

进行hash冲突判断

  • 发现在mArrays中index=4的Key,并不是我们要找的Key(发生了hash冲突)

接下来就要在这一组相同hash中查找和目标Key一样的Key

  • 先向右遍历查找,找到了在mArrays中index=6的Key,但仍然不是我们要找的Key(假设我们要找的Key应当是mArrays中index=2的Key)
  • 然后在向左遍历查找,最后找到了在mArrays中index=2的Key

如果在这一组相同hash中都查不到和目标Key一样的Key,就返回一个负数~end,表示之前并没有差如果这个Key。而且这个~end如果外界再次按位取反~~end就可以得到如果要插入该元素,应该插在哪个下标位置,进而移动数组,插入元素。

解释(2)

  • indexOf(Object key,int hash)

它的hash冲突处理方式与上述 解释(1) 中的思路一致。区别是传入参数,key是用来处理在hash冲突的情况下,找到正确的key。

hash直接使用的是key这个类自身的hashcode()或者系统的hash算法,不做额外处理:

hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
index = indexOf(key, hash);

还有一个区别是比较Key时不再为比较null,而是使用key.equals()来比较:

//hash为原始哈希值,可能是系统的,也可能是用户自定义hashcode()的,
// key可以用来equals()来判断处理哈希冲突
@UnsupportedAppUsage(maxTargetSdk = 28)
int indexOf(Object key, int hash) {
    final int N = mSize;
    //如果一个元素都没有,直接返回负数,找不到,下一个插入的mHashes[]的位置为0
    if (N == 0) {
        //从0变为 -2^32
        return ~0;
    }
    //如果数组非空,就可以进行二分查找来找到下标
    //需要注意的是,ArrayMap的int[] mHashes是用来存储keys的hash的
    //通过它的下标位置*2(+1)可以得到Key和Value在mArrays[]中的位置。
    //由于使用了hash,所以需要处理hash冲突,ArrayMap将相同的hash连续存放在mHashes[]中,可以遍历这一组hash对应的key来查找目标Key

    // 也就是不论K的类型是什么,都以其hashcode作为key,升序保存
    //底层通过ContainerHelper来二分查找,如果是负数,说明没找到
    //但是对该负数按位取反可以得到最后一个检索到的下标
    int index = binarySearchHashes(mHashes, N, hash);
    //如果没找到,返回一个负数
    if (index < 0) {
        return index;
    }

    //如果之前插入的key有这个hash值的,那么定位到这个Key所在的位置(index*2)
    if (key.equals(mArray[index << 1])) {//如果没有hash冲突,这个位置就是我们要找的key,直接返回
        return index;
    }

    
    int end;
    //ArrayMap将相同的hash连续存放在mHashes[]中,可以遍历这一组hash对应的key来查找目标Key
    //mHashes[]从index+1开始往后找相同hash对应的Key
    for (end = index + 1; end < N && mHashes[end] == hash; end++) {
        if (key.equals(mArray[end << 1])) return end;
    }

    //由于二分定位到的hash的下标可能是这一组相同hash的中间某个元素,所以也要往前找一找
    for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
        if (key.equals(mArray[i << 1])) return i;
    }

    //如果还是没找到,返回一个负数
    //~~end=end,可以用作插入的下标
    return ~end;
}

解释(3)

  • allocArrays(int size)

如果要扩容,就要根据新的长度去申请一块新的数组。源码中也发现了,扩容的逻辑是:

  • 如果旧size超过了8,就按1.5倍来扩容
  • 如果旧size在4和8之间,扩容到8
  • 如果旧size小于4,扩容到4
@UnsupportedAppUsage(maxTargetSdk = 28)
private void allocArrays(final int size) {
    if (mHashes == EMPTY_IMMUTABLE_INTS) {
        throw new UnsupportedOperationException("ArrayMap is immutable");
    }
    //如果需要扩容到8
    if (size == (BASE_SIZE * 2)) {
        synchronized (sTwiceBaseCacheLock) {
            if (mTwiceBaseCache != null) {
                //缓存(长度为4*2=8),不要再另外申请开辟空间了,这个缓存来自 freeArrays()
                final Object[] array = mTwiceBaseCache;
                //直接使用缓存空间
                mArray = array;
                try {
                    mTwiceBaseCache = (Object[]) array[0];
                    mHashes = (int[]) array[1];
                    if (mHashes != null) {
                        //如果都是空,就可用,return
                        array[0] = array[1] = null;
                        mTwiceBaseCacheSize--;
                        return;
                    }
                } catch (ClassCastException e) {
                }
                mTwiceBaseCache = null;
                mTwiceBaseCacheSize = 0;
            }
        }
    } else if (size == BASE_SIZE) {
        //如果新容量为4
        synchronized (sBaseCacheLock) {
            if (mBaseCache != null) {
                //使用缓存(长度为4),来自freeArrays()
                final Object[] array = mBaseCache;
                mArray = array;
                try {
                    mBaseCache = (Object[]) array[0];
                    mHashes = (int[]) array[1];
                    if (mHashes != null) {
                        array[0] = array[1] = null;
                        mBaseCacheSize--;
                        return;
                    }
                } catch (ClassCastException e) {
                }
                mBaseCache = null;
                mBaseCacheSize = 0;
            }
        }
    }
	//如果扩容到更大的容量,或者缓存不可用,就新申请一块空间
    mHashes = new int[size];
    mArray = new Object[size << 1];
}

由于开辟一个数组需要时间,对于只需要容量为4和8的两种情况,直接尝试复用即可,复用缓存的情况一般出现在减容的过程中,因为扩容时总会经历扩容到4和8的情况。减容时可以复用之前被free掉的4和8容量的缓存。超过8容量的扩容直接申请新的数组即可。

解释(4)

put()过程中,如果需要扩容,紧接着就到了freeArrays(),释放原有的数组。如果被释放的数组长度为4或者8就分别缓存到mBaseCache和mTwiceBaseCache,更大的就不管了。这个缓存可以被 解释(3) 使用。

@UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
    //如果要释放的数组元素长度为8
    if (hashes.length == (BASE_SIZE * 2)) {
        synchronized (sTwiceBaseCacheLock) {
            if (mTwiceBaseCacheSize < CACHE_SIZE) {
                //原有mArrays[0]转为指向mTwiceBaseCache,第一次为null
                array[0] = mTwiceBaseCache;
                //原有mArrays[1]转为指向hashes数组
                array[1] = hashes;
                //将mArrays中剩余元素置空
                for (int i = (size << 1) - 1; i >= 2; i--) {
                    //将原有元素置空
                    array[i] = null;
                }
                //这个mArrays[]被拿来复用,由mTwiceBaseCache记录
                //每个ArrayMap允许有一个mTwiceBaseCache实例
                mTwiceBaseCache = array;
                //总缓存数+1,是类变量,全局唯一,避免整个进程出现过多的缓存
                mTwiceBaseCacheSize++;
            }
        }
    } else if (hashes.length == BASE_SIZE) {
        //如果要释放的数组元素长度为4
        synchronized (sBaseCacheLock) {
            if (mBaseCacheSize < CACHE_SIZE) {
                //原有mArrays[0]转为指向mBaseCache,第一次为null
                array[0] = mBaseCache;
                //原有mArrays[1]转为指向hashes数组
                array[1] = hashes;
                //将mArrays中剩余元素置空
                for (int i = (size << 1) - 1; i >= 2; i--) {
                    array[i] = null;
                }
                //这个mArrays[]被拿来复用,由mBaseCache记录
                //每个ArrayMap允许有一个mBaseCache实例
                mBaseCache = array;
                //总缓存数+1,是类变量,全局唯一,避免整个进程出现过多的缓存
                mBaseCacheSize++;
            }
        }
    }
}

需要注意的是,整个进程中,mBaseCache和mTwiceBaseCache的数量都不允许超过 10。假设原先数组长度为4,但是需要扩容到8,则需要释放的两个数组被作为缓存,这个缓存的结构如下图所示:

请添加图片描述

可以通过arrayMap.mBaseCache访问到被缓存的mArrays和mHashes,但mBaseCache在一个进程中不允许超过10个,mTwiceBaseCache也是一样的。

需要注意的是,这里的mHashes并没有清空,而mArrays被清空了。笔者认为有两个原因:

  • 原因之一就是处理内存泄漏,mArrays对元素都是强引用,需要主动置空释放引用。mHashes没有引用问题,所以不用释放。

  • 另一个原因就是在减容的过程中,如果复用了缓存,将会把新的数据填充进去,原先的mHashes的值会被覆盖,所以不用额外耗费精力去清空mHashes。

3. 删除

  • V remove(Object key)
  • V removeAt(int index)

根据key删除元素,如果元素存在,将Value返回出去。删除元素过程中可能发生减容!!! 需要注意的是mHashes.length 和 mSize 是不同的。 mSize是真实元素个数, mHashes.length是容量。

如果发生了减容,并且减容后的容量为4或者8,就可能复用缓存。复用缓存的时候可以会把旧数组的数据拷贝进去。

@Override
public V remove(Object key) {
    //根据key找到元素在mHashes的下标,如果没找到返回负数
    final int index = indexOfKey(key);
    if (index >= 0) {
        //找到了该key在mHashes中的下标,根据index来删除元数,并返回value值
        return removeAt(index);
    }
    return null;
}

//删除元素
public V removeAt(int index) {
    //越界判断
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        throw new ArrayIndexOutOfBoundsException(index);
    }
    //记录一下 旧Value
    final Object old = mArray[(index << 1) + 1];
    //旧size
    final int osize = mSize;
    //计算新size
    final int nsize;
    if (osize <= 1) {//删完后,为空
        // Now empty.
        //旧 mHashes[]
        final int[] ohashes = mHashes;
        //旧 mArrays[]
        final Object[] oarray = mArray;
        mHashes = EmptyArray.INT;
        mArray = EmptyArray.OBJECT;
        //尝试将原有数组标为缓存备用(如果原数组容量为4或8,且全局缓存不超过10个)
        freeArrays(ohashes, oarray, osize);
        nsize = 0;
    } else {
        //如果删完还剩下元素
        nsize = osize - 1;
        //如果空间浪费过多,就减容!
        //需要注意的是mHashes.length 和 mSize 是不同的。 mSize是真实元素个数, mHashes.length是容量
        if (mHashes.length > (BASE_SIZE * 2) && mSize < mHashes.length / 3) {
            //如果当前容量比BASE_SIZE*2来的大,但真实元素个数又没到当前容量的1/3
            //就减容!!! 核心目的就是为了减少内存消耗
            final int n = osize > (BASE_SIZE * 2) ? (osize + (osize >> 1)) : (BASE_SIZE * 2);
            //旧数据
            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            //申请一块减容后长度的数组
            //如果减容后发现n=4或者8,可以尝试直接使用缓存
            allocArrays(n);

            if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
                throw new ConcurrentModificationException();
            }
            //拷贝时,就不拷贝该元素
            //先把这个元素之前的拷贝进去
            if (index > 0) {
                if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
                System.arraycopy(ohashes, 0, mHashes, 0, index);
                System.arraycopy(oarray, 0, mArray, 0, index << 1);
            }
            //再把这个元素之后的元素拷贝进去
            if (index < nsize) {
                if (DEBUG) Log.d(TAG, "remove: copy from " + (index + 1) + "-" + nsize
                                 + " to " + index);
                System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
                                 (nsize - index) << 1);
            }
        } else {
            //如果不需要减容,就把后续的元素往前拷贝一位就够了,把要删除的元素覆盖掉
            if (index < nsize) {
                if (DEBUG) Log.d(TAG, "remove: move " + (index + 1) + "-" + nsize
                                 + " to " + index);
                System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
                System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
                                 (nsize - index) << 1);
            }
            //将最后一位清空,防止引用错误、以及辅助GC
            mArray[nsize << 1] = null;
            mArray[(nsize << 1) + 1] = null;
        }
    }
    if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
        throw new ConcurrentModificationException();
    }
    //更新最新长度
    mSize = nsize;
    //返回旧的数据
    return (V) old;
}

4. 查找

  • V get(Object key)

插入部分分析完之后,剩下的逻辑就非常简单了。查找的逻辑就两步,根据key查找元素的hash所在mHashes[]的下标index,其Value位置对应为index*2+1

@Override
public V get(Object key) {
    //查找Key的hash在mHashes[]中的下标
    final int index = indexOfKey(key);
    //如果没找到,返回null,如果找到了,返回value
    return index >= 0 ? (V) mArray[(index << 1) + 1] : null;
}

5. 根据index查找Key或者Value

  • K keyAt(int index)
  • V valueAt(int index)

这部分代码也非常好理解,先是防越界(这里的界不是容量的界,而是真实数据的界,为什么要这样呢?因为有可能用到缓存,mHashes中,不再mSize范围中的部分会有脏数据!)然后就是根据下标计算拿到在mArrays中的Key或者Value

public K keyAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        // The array might be slightly bigger than mSize, in which case, indexing won't fail.
        // Check if exception should be thrown outside of the critical path.
        throw new ArrayIndexOutOfBoundsException(index);
    }
    return (K) mArray[index << 1];
}

public V valueAt(int index) {
    if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
        // The array might be slightly bigger than mSize, in which case, indexing won't fail.
        // Check if exception should be thrown outside of the critical path.
        throw new ArrayIndexOutOfBoundsException(index);
    }
    return (V) mArray[(index << 1) + 1];
}

6. append() 插入

  • ``append(K key, V value)`

和SparseArray的设计类似,如果连续插入hash递增的Key的元素,每次都二分查找效率是不够的,允许直接和当前最大hash值做判断。要么在末尾插入,要么二分查找合适的插入位置。

@UnsupportedAppUsage(maxTargetSdk = 28) // Storage is an implementation detail. Use put(K, V).
public void append(K key, V value) {
    int index = mSize;
    //先拿到key的hash值
    final int hash = key == null ? 0
        : (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
    if (index >= mHashes.length) {
        //如果插入会发生越界,直接报错,不会智能地去扩容
        throw new IllegalStateException("Array is full");
    }
    if (index > 0 && mHashes[index - 1] > hash) {
        //如果key小,就正常的put
        put(key, value);
        return;
    }
    //否则,直接在最右边插入新元素即可
    mSize = index + 1;
    mHashes[index] = hash;
    index <<= 1;
    mArray[index] = key;
    mArray[index + 1] = value;
}

9. 遍历ArrayMap

  • K keyAt(int index)
  • V valueAt(int index)
  • int size()

在Parcel.writeArrayMapInternal()中的使用:

final int N = map.size();
for(int i = 0; i < N ; i++){
    writeString(map.keyAt(i));
    writeValue(map.valueAt(i));
}

8. validate()

  • validate()

使用append()可能导致ArrayMap失效,因为可能存在一样的Key出现在多处,validate()方法可以判断这个ArrayMap是否合法,如果有多处相同Key的问题存在,就抛出错误。主要用途还是用于判断经过IPC的unpack后的ArrayMap是否合法。也是因为IPC通信在发送端可能会有并发问题。

Android中的Bundle数据就是用ArrayMap来存储的

BaseBundle.mMap是ArrayMap<String,Object>类型的

最后会将mMap的数据写入Parcel

parcel.writeArrayMapInternal(map)

public void validate() {
    final int N = mSize;
    //如果只有一个元素,不可能出现重复Key的情况
    if (N <= 1) {
        // There can't be dups.
        return;
    }
    //需要前后比较hash是否相同,所以要先拿到第一个的hash值
    int basehash = mHashes[0];
    int basei = 0;
    for (int i = 1; i < N; i++) {
        int hash = mHashes[i];
        //如果遍历到的hash和前一个不一样
        if (hash != basehash) {
            //更新
            basehash = hash;
            basei = i;
            continue;
        }
        //如果hash一样,说明当时插入的时候出现了hash冲突
        // 这就需要检查Key是否equals()了
        //先拿到cur = Key
        final Object cur = mArray[i << 1];
        //从hash冲突的index开始往前看,是否存在相同的Key
        for (int j = i - 1; j >= basei; j--) {
            final Object prev = mArray[j << 1];
            if (cur == prev) {//如果Key相同,说明之前出现了并发错误
                throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
            }
            if (cur != null && prev != null && cur.equals(prev)) {
                //如果都是非空元素,但是Key相同,说明之前出现了并发错误
                throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
            }
        }
        //如果一切正常,这个方法就不会抛出任何异常。
    }
}

总结

ArrayMap与SparseArray、HashMap、TreeMap的时间复杂度分析:

任务ArrayMap说明SparseArrayHashMap
删除O(N)数组拷贝O(logN)O(1)
顺序插入O(1) or O(N)O(1):append直接放在最后
O(N):移动数组
O(1) or O(N)O(1)
随机插入O(logN) or O(N)O(N):插入前可能要移动数组
O(logN):二分查找插入位置
O(logN) or O(N)O(1)
逆序插入O(N)O(logN):二分查找插入位置
O(N):必然要移动数组元素
O(N)O(1)
更新O(logN)O(logN):如果需要更新已存在的元素,只要二分查找的时间O(logN)O(1)

再来测算一下内存占用:

//TreeMap
Entry<K,V> root;// 21 * N Byte
class Entry<K,V>{//至少21 Byte
    K key;//4 Byte
    V value;//4 Byte
    Entry<K,V> left;//4 Byte
    Entry<K,V> right;//4 Byte
    Entry<K,V> parent;//4 Byte
    boolean color = BLACK;//1 Byte
}
//HashMap
Node<K,V>[] table;//16 * N Byte
class Node<K,V>{//至少16 Byte
    final int hash;//4 Byte
    final K key;//4 Byte
    V value;//4 Byte
    Node<K,V> next;//4 Byte
}
//SparseArray
int[] mKeys;//4 * N Byte
Object[] mValues;//4 * N Byte
//ArrayMap
int[] mHashes;//4 * N Byte
Object[] mArrays;//4 * 2* N Byte

如果不考虑缓存等内存占用:

  • TreeMap一个元素至少占用 21 Byte
  • HashMap一个元素至少占用 16 Byte
  • SparseArray一个元素至少占用 8 Byte
  • ArrayMap一个元素至少占用 12 Byte

ArrayMap虽然内存占用比SparseArray多,但其Key可以为任意类型,不局限于Integer,在这个情况下,与Key为任意类型的HashMap相比,又减少了许多内存占用。

所以,如果没有速度需求,尽量减少内存占用时:

  • 如果Key为int类型,就使用SparseArray
  • 如果Key为任意类型,就使用ArrayMap

随机插入的情况下,在10_000数据量下,HashMap、SparseArray、ArrayMap的执行速度差别并不大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值