資料結構HashMap(Android SparseArray 和ArrayMap)

codeGoogle發表於2018-06-20

HashMap也是我們使用非常多的Collection,它是基於雜湊表的 Map 介面的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash演算法來來計算key-value的儲存位置,我們總是可以通過key快速地存、取value。

HashMap

HashMap.java原始碼分析:  三個建構函式:  HashMap():預設初始容量capacity(16),預設載入因子factor(0.75)  HashMap(int initialCapacity):構造一個帶指定初始容量和預設載入因子 (0.75) 的空 HashMap。  HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和載入因子的空 HashMap。

    /**
     * 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…總結的可以。

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值不等 ,則將該節點插入該連結串列的鏈頭。

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鏈了。

二、擴容問題。
隨著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資料,所以效率相對要高。
複製程式碼
  • 3、ArrayMap提供了陣列收縮的功能,只要判斷過判斷容量尺寸,例如clear,put,remove等方法,只要通過判斷size大小觸發到freeArrays或者allocArrays方法,會重新收縮陣列,釋放空間。

  • 4、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)面試題級答案(精選版)

技術+職場

相關文章