数据结构HashMap(Android SparseArray 和ArrayMap)

HashMap也是咱们使用很是多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在。在HashMap中,key-value老是会当作一个总体来处理,系统会根据hash算法来来计算key-value的存储位置,咱们老是能够经过key快速地存、取value。html

HashMap

HashMap.java源码分析:  三个构造函数:  HashMap():默认初始容量capacity(16),默认加载因子factor(0.75)  HashMap(int initialCapacity):构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。  HashMap(int initialCapacity, float loadFactor):构造一个带指定初始容量和加载因子的空 HashMap。java

/**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    //构建自定义初始容量的构造函数,默认加载因子0.75的HashMap
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    //构造一个带指定初始容量和加载因子的空 HashMap
    public HashMap(int initialCapacity, float loadFactor) {
    ...
    ...
    }

复制代码

HashMap内部是使用一个默认容量为16的数组来存储数据的,而数组中每个元素却又是一个链表的头结点,因此,更准确的来讲,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表),如图:  这种存储数据的方法叫作拉链法  面试

这里写图片描述

且每个结点都是Entry类型,那么Entry是什么呢?咱们来看看HashMap中Entry的属性:算法

final K key; //key值
V value; //value值
HashMapEntry<K,V> next;//next下一个Entry
int hash;//key的hash复制代码
快速存取

put(key,value);数组

public V put(K key, V value) {
        if (table == EMPTY_TABLE) {//判断table空数组,
            inflateTable(threshold);//建立数组容量为threshold大小的数组,threshold在HashMap构造函数中赋值initialCapacity(指定初始容量);
        }
        //当key为null,调用putForNullKey方法,保存null与table第一个位置中,这是HashMap容许key为null的缘由
        if (key == null)
            return putForNullKey(value); 
        int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); //计算key的hash值
        int i = indexFor(hash, table.length); //计算key hash 值在 table 数组中的位置
         //从i出开始迭代 e,找到 key 保存的位置
        for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判断该条链上是否有hash值相同的(key相同)
            //若存在相同,则直接覆盖value,返回旧value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;//旧值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回覆盖后的旧值
            }
        }

        //修改次数增长1
        modCount++;
        //将key、value添加至i位置处
        addEntry(hash, key, value, i);
        return null;
    }
复制代码

put过程分析:这篇文章www.cnblogs.com/chenssy/p/3…总结的能够。bash

put过程结论:  当咱们想一个HashMap中添加一对key-value时,系统首先会计算key的hash值,而后根据hash值确认在table中存储的位置。若该位置没有元素,则直接插入。不然迭代该处元素链表并依此比较其key的hash值。若是两个hash值相等且key值相等(e.hash == hash && ((k = e.key) == key || key.equals(k))),则用新的Entry的value覆盖原来节点的value。若是两个hash值相等但key值不等 ,则将该节点插入该链表的链头。app

void addEntry(int hash, K key, V value, int bucketIndex) {
        //获取bucketIndex处的Entry
        Entry<K, V> e = table[bucketIndex];
        //将新建立的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry 
        table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
        //若HashMap中元素的个数超过极限了,则容量扩大两倍
        if (size++ >= threshold)
            resize(2 * table.length);
    }
复制代码

这个方法中有两点须要注意:框架

一是链的产生。这是一个很是优雅的设计。系统老是将新的Entry对象添加到bucketIndex处。若是bucketIndex处已经有了对象,那么新添加的Entry对象将
指向原有的Entry对象,造成一条Entry链,可是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。

2、扩容问题。
随着HashMap中元素的数量愈来愈多,发生碰撞的几率就愈来愈大,所产生的链表长度就会愈来愈长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必需要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度*加载因子。可是扩容是一个很是耗时的过程,由于它须要从新计算这些数据在新table数组中的位置并进行复制处理。因此若是咱们已经预知HashMap中元素的个数,那么预设元素的个数可以有效的提升HashMap的性能。

复制代码

读取实现:get(key)  相对于HashMap的存而言,取就显得比较简单了。经过key的hash值找到在table数组中的索引处的Entry,而后返回该key对应的value便可。函数

public V get(Object key) {
        // 若为null,调用getForNullKey方法返回相对应的value
        if (key == null)
            return getForNullKey();
        // 根据该 key 的 hashCode 值计算它的 hash 码  
        int hash = hash(key.hashCode());
        // 取出 table 数组中指定索引处的值
        for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
            Object k;
            //若搜索的key与查找的key相同,则返回相对应的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
                return e.value;
        }
        return null;
    }
复制代码

在不断的向HashMap里put数据时,当达到必定的容量限制时(这个容量知足这样的一个关系时候将会扩容:HashMap中的数据量>容量*加载因子,而HashMap中默认的加载因子是0.75),HashMap的空间将会扩大;扩大以前容量的2倍 :resize(newCapacity)源码分析

int newCapacity = table.length;//赋值数组长度
newCapacity <<= 1;//x2
if (newCapacity > table.length)
  resize(newCapacity);//调整HashMap大小容量为以前table的2倍
复制代码

这也就是重点所在,为何在Android上须要使用SparseArray和ArrayMap代替HashMap,主要缘由就是Hashmap随着数据不断增多,达到最大值时,须要扩容,并且扩容的大小是以前的2倍.

SparseArray

SparseArray.java 源码  SparseArray比HashMap更省内存,在某些条件下性能更好,主要是由于它避免了对key的自动装箱(int转为Integer类型),它内部则是经过两个数组来进行数据存储的,一个存储key,另一个存储value,为了优化性能,它内部对数据还采起了压缩的方式来表示稀疏数组的数据,从而节约内存空间,咱们从源码中能够看到key和value分别是用数组表示:

private int[] mKeys;//int 类型key数组
private Object[] mValues;//value数组
复制代码

构造函数:  SparseArray():默认容量10;  SparseArray(int initialCapacity):指定特定容量的SparseArray

public SparseArray() {
        this(10);
    }

public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {//判断传入容量值
            mKeys = EmptyArray.INT;
            mValues = EmptyArray.OBJECT;
        } else {//不为0初始化key value数组
            mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
            mKeys = new int[mValues.length];
        }
        mSize = 0;//mSize赋值0
    }
复制代码

从上面建立的key数组:SparseArray只能存储key为int类型的数据,同时,SparseArray在存储和读取数据时候,使用的是二分查找法;

/**
* 二分查找,中间位置的值与须要查找的值循环比对
* 小于:范围从mid+1 ~ h1
* 大于:范围从0~mid-1
* 等于:找到值返回位置mid
*/
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
    }
复制代码
SparseArray存取数据

SparseArray的put方法:

public void put(int key, E value) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//二分查找数组mKeys中key存放位置,返回值是否大于等于0来判断查找成功
        if (i >= 0) {//找到直接替换对应值
            mValues[i] = value;
        } else {//没有找到
            i = ~i;//i按位取反获得非负数

            if (i < mSize && mValues[i] == DELETED) {//对应值是否已删除,是则替换对应键值
                mKeys[i] = key; 
                mValues[i] = value;
                return;
            }

            if (mGarbage && mSize >= mKeys.length) {//当mGarbage == true 而且mSize 大于等于key数组的长度
                gc(); //调用gc回收

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

            //最后将新键值插入数组,调用 GrowingArrayUtils的insert方法:
            mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
            mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
            mSize++;
        }
        }

复制代码

下面进去看看 GrowingArrayUtils的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;
          }
        //大于数组长度须要进行扩容
        T[] newArray = (T[]) Array.newInstance(array.getClass().getComponentType(),
        growSize(currentSize));//扩容规则里面就一句三目运算:currentSize <= 4 ? 8 : currentSize * 2;(扩容2倍)
        System.arraycopy(array, 0, newArray, 0, index);
        newArray[index] = element;
        System.arraycopy(array, index, newArray, index + 1, array.length - index);
        return newArray;
    }

复制代码

SparseArray的get(key)方法:

public E get(int key) {
        return get(key, null);//调用get(key,null)方法
    }

public E get(int key, E valueIfKeyNotFound) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);//二分查找key

        if (i < 0 || mValues[i] == DELETED) {//没有找到,或者已经删除返回null
            return valueIfKeyNotFound;
        } else {//找到直接返回i位置的value值
            return (E) mValues[i];
        }
    }
复制代码

SparseArray在put添加数据的时候,会使用二分查找法和以前的key比较当前咱们添加的元素的key的大小,而后按照从小到大的顺序排列好,因此,SparseArray存储的元素都是按元素的key值从小到大排列好的。  而在获取数据的时候,也是使用二分查找法判断元素的位置,因此,在获取数据的时候很是快,比HashMap快的多,由于HashMap获取数据是经过遍历Entry[]数组来获得对应的元素。

SparseArray应用场景:

虽然说SparseArray性能比较好,可是因为其添加、查找、删除数据都须要先进行一次二分查找,因此在数据量大的状况下性能并不明显,将下降至少50%。

知足下面两个条件咱们可使用SparseArray代替HashMap:

  • 数据量不大,最好在千级之内
  • key必须为int类型,这中状况下的HashMap能够用SparseArray代替:

ArrayMap

ArrayMap是一个

public class ArrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V> {}
复制代码

构造函数由父类实现:

public ArrayMap() {
        super();
    }

    public ArrayMap(int capacity) {
        super(capacity);
    }

    public ArrayMap(SimpleArrayMap map) {
        super(map);
    }

复制代码

HashMap内部有一个HashMapEntry[]对象,每个键值对都存储在这个对象里,当使用put方法添加键值对时,就会new一个HashMapEntry对象,而ArrayMap的存储中没有Entry这个东西,他是由两个数组来维护的,mHashes数组中保存的是每一项的HashCode值,mArray中就是键值对,每两个元素表明一个键值对,前面保存key,后面的保存value。

int[] mHashes;//key的hashcode值
 Object[] mArray;//key value数组
复制代码

这里写图片描述

SimpleArrayMap():建立一个空的ArrayMap,默认容量为0,它会跟随添加的item增长容量。  SimpleArrayMap(int capacity):指定特定容量ArrayMap;  SimpleArrayMap(SimpleArrayMap map):指定特定的map;

public SimpleArrayMap() {
        mHashes = ContainerHelpers.EMPTY_INTS;
        mArray = ContainerHelpers.EMPTY_OBJECTS;
        mSize = 0;
    }
    ...
复制代码
ArrayMap 存取

ArrayMap 的put(K key, V value):key 不为null

/**
     * Add a new value to the array map.
     * @param key The key under which to store the value.  <b>Must not be null.</b>  If
     * this key already exists in the array, its value will be replaced.
     * @param value The value to store for the given key.
     * @return Returns the old value that was stored for the given key, or null if there
     * was no such key.
     */
public V put(K key, V value) {
        final int hash;
        int index;
        //key 不能为null
        if (key == null) { //key == null,hash为0 
            hash = 0; 
            index = indexOfNull();
        } else {//获取key的hashhash = key.hashCode();
            index = indexOf(key, hash);//获取位置
        }
        //返回index位置的old值
        if (index >= 0) {
            index = (index<<1) + 1;
            final V old = (V)mArray[index];//old 赋值 value
            mArray[index] = value;
            return old;
        }
        //不然按位取反
        index = ~index;
        //扩容  System.arrayCopy数据
        if (mSize >= mHashes.length) {
            final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1))
                    : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);

            if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);

            final int[] ohashes = mHashes;
            final Object[] oarray = mArray;
            allocArrays(n);//申请数组

            if (mHashes.length > 0) {
                if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0");
                System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
                System.arraycopy(oarray, 0, mArray, 0, oarray.length);
            }

            freeArrays(ohashes, oarray, mSize);//从新收缩数组,释放空间
        }

        if (index < mSize) {
            if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index)
                    + " to " + (index+1));
            System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index);
            System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
        }
        //最后 mHashs数组存储key的hash值
        mHashes[index] = hash;
        mArray[index<<1] = key;//mArray数组相邻位置存储key 和value值
        mArray[(index<<1)+1] = value;
        mSize++;
        return null;
    }
复制代码

从最后能够看出:ArrayMap的存储中没有Entry这个东西,他是由两个数组来维护的,mHashes数组中保存的是每一项的HashCode值,mArray中就是键值对,每两个元素表明一个键值对,前面保存key,后面的保存value。

ArrayMap 的get(Object key):从Array数组得到value

/**
     * Retrieve a value from the array.
     * @param key The key of the value to retrieve.
     * @return Returns the value associated with the given key,
     * or null if there is no such key.
     */
    public V get(Object key) {
        final int index = indexOfKey(key);//得到key在Array的存储位置
        return index >= 0 ? (V)mArray[(index<<1)+1] : null;//若是index>=0 取(index+1)上的value值,不然返回null(从上面put知道array存储是key(index) value(index+1)存储的)
    }
复制代码

ArrayMap 和 HashMap区别:

  • 1.存储方式不一样
HashMap内部有一个HashMapEntry[]对象,每个键值对都存储在这个对象里,当使用put方法添加键值对时,就会new一个HashMapEntry对象

ArrayMap的存储中没有Entry这个东西,他是由两个数组来维护的
mHashes数组中保存的是每一项的HashCode值,
mArray中就是键值对,每两个元素表明一个键值对,前面保存key,后面的保存value。
复制代码
  • 2.添加数据时扩容时的处理不同
HashMap使用New的方式申请空间,并返回一个新的对象,开销会比较大
ArrayMap用的是System.arrayCopy数据,因此效率相对要高。
复制代码
  • 三、ArrayMap提供了数组收缩的功能,只要判断过判断容量尺寸,例如clear,put,remove等方法,只要经过判断size大小触发到freeArrays或者allocArrays方法,会从新收缩数组,释放空间。

  • 四、ArrayMap相比传统的HashMap速度要慢,由于查找方法是二分法,而且当你删除或者添加数据时,会对空间从新调整,在使用大量数据时,效率低于50%。能够说ArrayMap是牺牲了时间换区空间。但在写手机app时,适时的使用ArrayMap,会给内存使用带来可观的提高。ArrayMap内部仍是按照正序排列的,这时由于ArrayMap在检索数据的时候使用的是二分查找,因此每次插入新数据的时候ArrayMap都须要从新排序,逆序是最差状况;

HashMap ArrayMap SparseArray性能测试对比(转载 )

直接看:www.jianshu.com/p/7b9a1b386…测试对比

1.插入性能时间对比 

这里写图片描述

数据量小的时候,差别并不大(固然了,数据量小,时间基准小,确实差别不大),当数据量大于5000左右,SparseArray,最快,HashMap最慢,乍一看,好像SparseArray是最快的,可是要注意,这是顺序插入的。也就是SparseArray和Arraymap最理想的状况。

这里写图片描述

倒序插入:数据量大的时候HashMap远超Arraymap和SparseArray,也前面分析一致。  固然了,数据量小的时候,例如1000如下,这点时间差别也是能够忽略的。

这里写图片描述

SparseArray在内存占用方面的确要优于HashMap和ArrayMap很多,经过数据观察,大体节省30%左右,而ArrayMap的表现正如前面说的,优化做用有限,几乎和HashMap相同。

2.查找性能对比

这里写图片描述

这里写图片描述

如何选择使用

  • 1.在数据量小的时候通常认为1000如下,当你的key为int的时候,使用SparseArray确实是一个很不错的选择,内存大概能节省30%,相比用HashMap,由于它key值不须要装箱,因此时间性能平均来看也优于HashMap,建议使用!

  • 2.ArrayMap相对于SparseArray,特色就是key值类型不受限,任何状况下均可以取代HashMap,可是经过研究和测试发现,ArrayMap的内存节省并不明显,也就在10%左右,可是时间性能确是最差的,固然了,1000之内的若是key不是int 能够选择ArrayMap。

参考:  MVC,MVP 和 MVVM 模式如何选择?

 一招教你读懂JVM和Dalvik之间的区别

个人Android重构之旅:框架篇

NDK项目实战—高仿360手机助手之卸载监听

(Android)面试题级答案(精选版)

技术+职场
相关文章
相关标签/搜索