1. 程式人生 > >Java中的集合之HashMap、LinkedHashMap、HashTable

Java中的集合之HashMap、LinkedHashMap、HashTable

Java集合之HashMap、LinkedHashMap、HashTable

討論集合關注的問題:

  1. 底層資料結構
  2. 增刪改查方式
  3. 初始容量,擴容方式,擴容時機
  4. 執行緒安全與否
  5. 是否允許空,是否允許重複,是否有序

文章目錄


我們都知道Collection介面派生出三大類的子介面List,Set和Queue。今天來看看另一個派系,沒錯就是Map。先來複習一下這張集合關係網(不全):

timg

可以看到,Map作為Collection的“生產者”,在另一種形式也就是通常說的非線性結構集合——鍵值對,具有很好的拓展空間。這個系列主要用來儲存非線性的集合資料型別。常用的有HashMap,HashTable,TreeMap等。下面詳細討論其特點和原始碼分析。一般在開發中,我們常用Map介面來動態使用這些類。

 

HashMap

作為最常用的Map型資料結構,HashMap採用了Hash的方式來儲存資料,能夠快速的獲取儲存位置的資料。

來看原始碼,HashMap繼承自AbstractMap抽象類,同時實現了Map,Cloneable和Serializable介面,說明HashMap可以實現克隆和序列化等操作。其內部實現了Node<K,V>的內部類,來存放資料。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value =
value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }

HashMap可以指定容量大小和一個載入因子進行初始化,預設的容量大小為16,載入因子為0.75;這裡載入因子loadFactor的概念是新出現的,表示這個散列表中使用的程度,當超過容量的這個百分比值就會進行擴容。

HashMap的內部使用一個table陣列進行鍵的維護,這個表也就是hash的桶(即初始容量大小),不同的位置存放著不同的hash值的資料。其中,多個hash值相同的元素,將會使用一個連結串列的形式連線在一起,使用偏移量作為標記。

1545795134820

HashMap中的Hash函式如下:

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

上述說明中,HashMap在進行初始化時,會使用容量大小進行table的初始化,但是這個表的大小是選擇滿足大於這個初始值的最小2的冪來實現的,我們在建構函式中可以看出來:

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);
    }
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;    //|=相當於||操作   >>>無符號右移,忽略符號位,空位都以0補齊
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap和元素存取實現過程:獲取到元素的key的雜湊值,然後通過雜湊值放入到table不同的位置中,如果該位置為null,表示第一個放入的;否則遍歷該位置的連結串列,將這個鍵放到最前(在jdk1.8之前是插入頭部的,在jdk1.8中是插入尾部的)。當然這個過程要比較鍵相同的hash值的value是否一致。【如下,在鏈的長度超過8時,將會轉為紅黑樹】

	public V put(K key, V value) {
        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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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);
                        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的讀取過程和儲存過程類似,先獲取key的雜湊值,在table中對應的位置;然後遍歷該鏈獲取相同的key 的值即可。HashMap刪除資料時,也是和上述類似過程,不過在清空資料時,則直接把table中的值全都置為Null,並沒有將鏈中的節點也都置為null。

HashMap中能夠將Key和Value作為單獨的集合,返回一個Collection的引用,這一方法的支援是源自其父類AbstractMap中實現了Map介面的keySet和Values方法。其中Key返回的是一個集合,不會有重複值,而values返回可以有重複值。


 

LinkedHashMap

LinkedHashMap直接繼承自HashMap類,實現了Map介面。從名字中,可以看出來,LinkedHashMap也是一種HashMap,只是其內部維護了一個雙向連結串列來儲存資料。其基本的存取和刪除方式與HashMap並無太大差異。

其實HashMap是無序的,通過迭代器所得元素的順序並不是它們最初放置到HashMap的順序。在它的內部,使用繼承自HashMap中Node的Entry鏈,來維護資料的有序性,也就是犧牲了時間和空間來維護。在原始碼中,可以看到有一個額外的head和一個tail元素的指標,分別指向Map的兩端。

AccessOrder欄位,是用來標記訪問順序的。true表示按照訪問順序迭代,false時表示按照插入順序。LinkedHashMap同樣是非執行緒同步的Map介面。

static class Entry<K,V> extends HashMap.Node<K,V> {    
        Entry<K,V> before, after; //額外的指標,維護順序
        Entry(int hash, K key, V value, Node<K,V> next) {  //next為內部的指向鏈
            super(hash, key, value, next);
        }
    }
	transient LinkedHashMap.Entry<K,V> head;
    transient LinkedHashMap.Entry<K,V> tail;

    final boolean accessOrder;

LinkedHashMap的存取。訪問get時,每次訪問到一個元素時,先將其指標修改,相當於放置到最末的位置,然後返回該資料的值。put方法直接繼承父類的方法。

	public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

可以用一張圖,來表示LinkedHashMap的內部結構

在這裡插入圖片描述

在這裡插入圖片描述

說明,在插入元素時,jdk1.8後,元素大於8就會轉為紅黑樹進行儲存,那麼這個雙向連結串列維護的同樣也是插入到紅黑樹的順序。LinkedHashMap的這個特性可以用來實現LRU演算法,可以參考底部的博文。


 

HashTable

和上述兩類集合一樣,HashTable同樣是Map系列的。HashTable的實現和HashMap非常類似,他們的關係有點向ArrayList和Vector。

HashTable繼承自Dictionary類,實現Map、Cloneable/Serializable介面。HashTable採用"拉鍊法"實現雜湊表,它定義了幾個重要的引數:table、count、threshold、loadFactor、modCount。table 為儲存元素的表,每個元素是一種繼承自Map.Entry類的Entry<K,V>型別。初始化容量和載入因子的概念和HashMap類似。

threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值=“容量*載入因子”

 private transient Entry<?,?>[] table;
 private transient int count;
 public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
 public Hashtable() {
        this(11, 0.75f);    //預設容量為11,載入因子為0.75
    }

在插入元素事,HashTable不允許Key和Value為空,這一點和HashMap不一樣。計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。值得注意的是,jdk1.8中,HashTable和HashMap不一樣,HashTable使用key的hashcode作為雜湊值。

public synchronized V get(Object key) {
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

插入操作如下,找到對應的鍵,插入到末尾。

 public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        addEntry(hash, key, value, index);
        return null;
    }

和HashMap還有一點不一樣的是,HashTable大部分方法被synchronized修飾了,所以其事執行緒安全的。


 

總結

  • LinkedHashMap在HashMap的陣列加連結串列結構的基礎上,將所有節點連成了一個雙向連結串列。當主動傳入的accessOrder引數為false時, 使用put方法時,新加入元素不會被加入雙向連結串列,get方法使用時也不會把元素放到雙向連結串列尾部。

  • 對比HashTable和HashMap

    • 我們從他們的定義就可以看出他們的不同,HashTable基於Dictionary類,而HashMap是基於AbstractMap。Dictionary是什麼?它是任何可將鍵對映到相應值的類的抽象父類,而AbstractMap是基於Map介面的骨幹實現,它以最大限度地減少實現此介面所需的工作。

    • HashMap可以允許存在一個為null的key和任意個為null的value,但是HashTable中的key和value都不允許為null。如下:當HashMap遇到為null的key時,它會呼叫putForNullKey方法來進行處理。對於value沒有進行任何處理,只要是物件都可以。

    • HashTable執行緒安全,HashMap快速失效,LinkedHashMap也非執行緒安全

參考文章:https://blog.csdn.net/a724888/article/details/80277176