Android中需要了解的資料結構(二)

skyxin888發表於2019-04-10

前言

前面瞭解完List介面的相關實現類 Android中需要了解的資料結構(一)

Map介面

Map與List、Set介面不同,它是由一系列鍵值對組成的集合,提供了key到Value的對映。在Map中它保證了key與value之間的一一對應關係。也就是說一個key對應一個value,所以它不能存在相同的key值,當然value值可以相同。
實現map的集合有:HashMap、HashTable、TreeMap、WeakHashMap。

HashMap

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {}
    
複製程式碼

HashMap繼承了Map,實現了map的所有方法。key和value允許使用全部的元素,包括null, 注意遍歷hashMap是隨機的,如果你想定義遍歷順序,請使用LinkedHashMap。
在Java言中,最基本的結構就是兩種,一個是陣列,另外一個是模擬指標(引用),所有的資料結構都可以用這兩個基本結構來構造的,HashMap也不例外。HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列的結合體。

Java8 對HashMap 底層做了優化 本文以Java8為例

  /**
    * An empty table instance to share when the table is not inflated.
    * Orcle的JDK中名字叫Node<K,V>
    */
    static final HashMapEntry<?,?>[] EMPTY_TABLE = {};

    /**
    The table, resized as necessary. Length MUST Always be a power of two.
    Orcle的JDK中名字叫Node<K,V>
    */
    transient HashMapEntry<K,V>[] table = (HashMapEntry<K,V>[]) EMPTY_TABLE;


    //Orcle的JDK
    /**
       * The table, initialized on first use, and resized as
       * necessary. When allocated, length is always a power of two.
       * (We also tolerate length zero in some operations to allow
       * bootstrapping mechanics that are currently not needed.)
       */
      transient Node<K,V>[] table;
    
      /**
       * Holds cached entrySet(). Note that AbstractMap fields are used
       * for keySet() and values().
       */
      transient Set<Map.Entry<K,V>> entrySet;
複製程式碼

陣列名叫table,初始化時為空。HashMapEntry/Node是HashMap的靜態內部類,資料節點都儲存在這裡面:

  static class HashMapEntry<K, V> implements Entry<K, V> {
      final K key;
      V value;
      final int hash;
      HashMapEntry<K, V> next;  
  }

  //java8
  static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;
          V value;
          Node<K,V> next;
  }
複製程式碼
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }
}
複製程式碼
    int threshold;// 所能容納的key-value對極限 
    final float loadFactor;//負載因子 預設0.75
    int modCount;  
    int size;
    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
    transient Node<K,V>[] table;
複製程式碼

HashMap是通過transient Node<K,V>[]table來儲存資料,Node就是陣列的元素,每個Node其實就是一個key-value對,它持有一個指向下一個元素的引用,這就構成了連結串列。

Android中需要了解的資料結構(二)

那麼為什麼要有連結串列呢?原因是為了解決 雜湊衝突 當我們新增或者查詢一個元素的時候,我們都會通過將我們的key的hashcode通過雜湊函式對映到陣列中的某個位置,通過陣列下標一次定位就可完成操作。
如果兩個不同的元素,通過雜湊函式得出的實際儲存地址相同怎麼辦?也就是說,當我們對某個元素進行雜湊運算,得到一個儲存地址,然後要進行插入的時候,發現已經被其他元素佔用了,其實這就是所謂的雜湊衝突,也叫雜湊碰撞。
雜湊衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的儲存地址),再雜湊函式法,鏈地址法,而HashMap即是採用了鏈地址法,也就是陣列+連結串列的方式。

HashMap中的核心put方法:

    public V put(K key, V value) {
        // 對key的hashCode()做hash
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //tab為空則建立
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //計算index,並對null做處理 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 節點key存在,直接覆蓋value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判斷該鏈為紅黑樹
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //該鏈為連結串列
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                         //連結串列長度大於8轉換為紅黑樹進行處理
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //超過最大容量 就擴容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
複製程式碼

HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在陣列中的位置(即下標),如果陣列該位置上已經存放有其他元素了,則通過key 的 equals 比較返回 true,新新增 Node 的 value 將覆蓋集合中原有 Node 的 value,但key不會覆蓋。如果這兩個 Node 的 key 通過 equals 比較返回 false,新新增的 Node 將與集合中原有 Node 形成 連結串列。

所以重寫equals方法必須要重寫hashcode方法

HashMap由陣列+連結串列組成的,陣列是HashMap的主體,連結串列則是主要為了解決雜湊衝突而存在的,如果定位到的陣列位置不含連結串列,那麼對於查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度為O(n),首先遍歷連結串列,存在即覆蓋,否則新增;對於查詢操作來講,仍需遍歷連結串列,然後通過key物件的equals方法逐一比對查詢。所以,效能考慮,HashMap中的連結串列出現越少,效能才會越好。

Hashtable

    public class Hashtable<K,V> extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable{}
    
    public Hashtable(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }
    public Hashtable(Map<? extends K, ? extends V> t) {
        this(Math.max(2*t.size(), 11), 0.75f);
        putAll(t);
    }
複製程式碼

Hashtable繼承Dictionary類,同樣是通過key-value鍵值對儲存資料的資料結構。 解決衝突時與HashMap也一樣也是採用了雜湊連結串列的形式,Hashtable和HashMap最大的不同是Hashtable的方法都是同步的,在多執行緒中,你可以直接使用Hashtable,而如果要使用HashMap,則必須要自己實現同步來保證執行緒安全。當然,如果你不需要使用同步的話,HashMap的效能是肯定優於Hashtable的。此外,HashMap是接收null鍵和null值的,而Hashtable不可以。

Hashtable於HashMap的區別

  • HashMap是繼承自AbstractMap類,而HashTable是繼承自Dictionary類,不過它們都實現了同時實現了map、Cloneable、Serializable這三個介面。
  • HashMap支援key或者value為null,而HashTable不支援。
  • Hashtable預設的初始大小為11,之後每次擴充,容量變為原來的2n+1。HashMap預設的初始化大小為16。之後每次擴充,容量變為原來的2倍。
  • 計算hash值的方法不同
         //Hashtable
         for (int i = oldCapacity ; i-- > 0 ;) {
             for (HashtableEntry<K,V> old = (HashtableEntry<K,V>)oldMap[i] ; old != null ; ){
                 HashtableEntry<K,V> e = old;
                 old = old.next;
    
                 int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                 e.next = (HashtableEntry<K,V>)newMap[index];
                 newMap[index] = e;
             }
         
         //HashMap
         static final int hash(Object key) {
             int h;
             return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
         }
    複製程式碼
    Hashtable在計算元素的位置時需要進行一次除法運算,而除法運算是比較耗時的。

TreeMap

    public class TreeMap<K,V> extends AbstractMap<K,V>
        implements NavigableMap<K,V>, Cloneable, java.io.Serializable{}
複製程式碼

有序雜湊表,實現SortedMap介面,底層通過紅黑樹實現。可以根據key的自然順序進行自動排序,當key是自定義物件時,TreeMap也可以根據自定義的Comparator進行排序。另外,TreeMap和HashMap一樣,也是非同步的。

相關文章