HashMap、ArrayMap、SparseArray

1、HashMap的数据结构

HashMap的数据结构为:数组+链表/红黑树
我们都知道HashMap中处理hash冲突的方法是链地址法,也就是说,如果有多个元素key的hash值相同的话,后一个元素并不会覆盖上一个元素,而是采取链表的方式,把之后加进来的元素加入链表末尾。
这里要注意的是,链表是单链表。
而至于红黑树是jdk1.8加进去的一个优化,也就是说桶中的结构可能是链表,也可能是红黑树,采用红黑树是为了提高效率,下面我们看一个HashMap的数据结构示意图(来源网上,侵删):

HashMap的数据结构

然后我们简单来看看HashMap中的几个常量,有助于我们对其进行理解:

//默认初始容量(16) 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大容量(2^30) 
static final int MAXIMUM_CAPACITY = 1 << 30; 
//默认加载因子 
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
//链表转换成树的阈值 
static final int TREEIFY_THRESHOLD = 8; 
//树转换成链表的阈值(执行resize操作时,当桶中bin的数量少于UNTREEIFY_THRESHOLD时使用链表来代替树) 
static final int UNTREEIFY_THRESHOLD = 6; 
//在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。 
//这是为了避免在哈希表建立初期,多个键值对恰 好被放入了同一个链表中而导致不必要的转化。 
static final int MIN_TREEIFY_CAPACITY = 64;

其中,加载因子做一下说明,意思就是:当HashMap中的数据量>容量x加载因子时,就会进行空间的扩大,而且扩大后新的空间一定是原来的2倍,所以,当数据量很大的情况下,将对内存空间造成很大消耗和浪费,而在Android中,我们对内存的控制会更加严格,所以,在某些特定场景下我们可以使用SparseArray和ArrayMap来代替HashMap。

2、初识ArrayMap、SparseArray

首先我们来看看HashMap、ArrayMap、SparseArray这三个类的全路径:

●HashMap:java.util.HashMap
●ArrayMap:android.util.ArrayMap
●SparseArray:android.util.SparseArray

看到区别了没?也就是说HashMap是jdk里面的类,而ArrayMap和SparseArray为Android独有的类
而且ArrayMap和SparseArray只能在API19以上的系统里面才有这个类,也就是Android4.4以上
当然,我们都知道Android碎片化严重,所以才有了support包,故而在一般情况下,为了兼容性,我们使用ArrayMap和SparseArray时会使用其兼容包,所以导包的时候请注意一点,以下为兼容包的类名:

●android.support.v4.util.ArrayMap
●android.support.v4.util.SparseArrayCompat

接着,我们来看看官方是如何说ArrayMap和SparseArray的:

ArrayMap is a generic key->value mapping data structure that is designed to be more memory efficient than a traditional HashMap, this implementation is a version of the platform's ArrayMap that can be used on older versions of the platform.
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.
Note that for containers holding up to hundreds of items, the performance difference is not significant, less than 50%.

从官方的说明中,我们知道两点:
A:ArrayMap和SparseArray会比HashMap更加节省内存空间
B:当数据量达到千级以上的时候,ArrayMap与SparseArray都要比HashMap效率更低50%

3、ArrayMap、SparseArray异同点

1)ArrayMap和SparseArray内部都是用两个数组来进行数据存储的
ArrayMap中的两个数组为:int[] mHashes和Object[] mArray,所以它的一个数组记录key的hash值,另外一个数组记录Value值
SparseArray的key值为int型的,所以它的两个数组为:int[] mKeys和Object[] mValues,很明显一个存key值,一个存value值
2)ArrayMap与SparseArray内部都使用了二分法进行从小到大的排序(数据量很大时,效率至少会低50%)
我们直接来看一下SparseArray中使用的二分法算法,很赞的算法(我居然看懂了),需要注意的一点,按源码我们知道,找不到key时返回小于0的数值,而不是返回-1

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
    }
4、HashMap、ArrayMap与SparseArray使用场景

直接上结论:
1)谷歌推荐数据量在千级以内时使用ArrayMap与SparseArray,数据量非常大时使用HashMap
2)如果数据量在千级以内,若key的类型已确定为int型,那么使用SparseArray,因为它避免了自动装箱的过程,效率会高些
3)如果数据量在千级以内,若key的类型已确定为int型,且value值确定为boolean或int或long,我们还可以直接使用SparseBooleanArraySparseIntArraySparseLongArray
4)如果数据量在千级以内,若key的类型已确定为long型,我们还可以使用LongSparseArray
5)如果数据量在千级以内,若key为其他类型,那么使用ArrayMap

5、实践

纸上得来终觉浅,理论说了一大堆,还是得上demo啊:

   public static void main(String[] args) {
        zhengMap();
        //zhengSparseArray();
        //zhengArrayMap();

        //fanMap();
        //fanSparseArray();
        //fanArrayMap();
  }

    private static int MAX=100000;

    private static void zhengMap(){
        HashMapmap = new HashMap<>();
        long start_map = System.currentTimeMillis();
        for(int i=0;i"+end_map+"<---Map占用的内存--->"+map_memory);
        /*<---Map的插入时间--->18<---Map占用的内存--->17553296*/
    }

    private static void fanMap(){
        HashMapmap = new HashMap<>();
        long start_map = System.currentTimeMillis();
        for(int i=MAX-1;i>=0;i--){
            map.put(i, String.valueOf(i));
        }
        long map_memory = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
        long end_map = System.currentTimeMillis()-start_map;
        System.out.println("<---Map的插入时间--->"+end_map+"<---Map占用的内存--->"+map_memory);
        /*<---Map的插入时间--->18<---Map占用的内存--->17553272*/
    }

    private static void zhengSparseArray(){
        SparseArrayCompat sparse = new SparseArrayCompat<>();
        long start_sparse = System.currentTimeMillis();
        for(int i=0;i"+end_sparse+"<---Sparse占用的内存--->"+sparse_memory);
        /*<---Sparse的插入时间--->15<---Sparse占用的内存--->13841288*/
    }

    private static void fanSparseArray(){
        SparseArrayCompat sparse = new SparseArrayCompat<>();
        long start_sparse = System.currentTimeMillis();
        for(int i=MAX-1;i>=0;i--){
            sparse.put(i, String.valueOf(i));
        }
        long sparse_memory = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
        long end_sparse = System.currentTimeMillis()-start_sparse;
        System.out.println("<---Sparse的插入时间--->"+end_sparse+"<---Sparse占用的内存--->"+sparse_memory);
        /*<---Sparse的插入时间--->2040<---Sparse占用的内存--->13841376*/
    }

    private static void zhengArrayMap(){
        ArrayMap arrayMap=new ArrayMap<>();
        long start_map = System.currentTimeMillis();
        for(int i=0;i"+end_map+"<---ArrayMap占用的内存--->"+map_memory);
        /*<---ArrayMap的插入时间--->16<---ArrayMap占用的内存--->17885040*/
    }

    private static void fanArrayMap(){
        ArrayMap arrayMap=new ArrayMap<>();
        long start_map = System.currentTimeMillis();
        for(int i=MAX-1;i>=0;i--){
            arrayMap.put(i, String.valueOf(i));
        }
        long map_memory = Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
        long end_map = System.currentTimeMillis()-start_map;
        System.out.println("<---ArrayMap的插入时间--->"+end_map+"<---ArrayMap占用的内存--->"+map_memory);
        /*<---ArrayMap的插入时间--->2909<---ArrayMap占用的内存--->18168432*/
    }

说起这个demo,还花了我不少时间,由于我们要对比的是HashMap、ArrayMap、SparseArray三者的执行效率及内存占用情况,时间计算这个简单得很,难的是内存的计算,笔者找了不少方案:
1)网上很多都推荐用Instrumentation,我表示搞不定
2)转byte[],算大小,HashMap还行,但其他两个因为没法序列化而无法进行
3)使用Android Profiler,这个理论上应该是可以的,毕竟工具很强大,可以具体看到某个对象在堆中占的大小,就是有点麻烦,你必须运行到Android系统上,而且得逐个分析找到那个对象
4)笔者一开始用的就是:Runtime.getRuntime().totalMemory(),结果怎么全部都一个样啊,后来才明白,这个是申请到的全部内存大小(单位字节),还需减去剩余的内存大小得到的才是已使用的,如上面demo所示。
需要注意一点就是,为了计算使用的内存大小,上面6个方法不能同时运行,得逐一运行,否则内存大小肯定算不准的。
各个方法的运行结果已在代码中注释出来了,为了更好的显示结果,我们列一个表格对比一下:

结果统计

好啦,这个表已经很清楚了,我们可以得出:
1)HashMap无论正序插入还是倒序插入,它的效率几乎没什么影响,这得益于它有非常好的处理冲突的方式,不需要遍历每一个值
2)ArrayMap、SparseArray在正序插入时效率挺好的,但倒序插入就非常糟糕了,这是因为每次插入都需要调用二分查找
3)比较HashMap和SparseArray,很明显,无论怎么插入,SparseArray占用内存确实小了,优势很明显
4)再看ArrayMap的占用内存的情况,好吧,这····好像有点打脸,跟理论还是存在一定的差距的(若有朋友知道原因还请告知,不胜感激!)
5)这个例子有100000数据量,其实并不适合使用ArrayMap和SparseArray

你可能感兴趣的:(HashMap、ArrayMap、SparseArray)