【Java入門提高篇】Day24 Java容器類詳解(七)HashMa

johnchou發表於2021-09-09

前兩篇對HashMap這傢伙的主要方法,主要演算法做了一個詳細的介紹,本篇主要介紹HashMap中默默無聞地工作著的集合們,包括KeySet,values,EntrySet,以及對應的迭代器:HashIterator,KeyIterator,ValueIterator,EntryIterator和 fast-fail 機制。會介紹三個集合的作用以及它們中隱藏的驚人秘密。

KeySet

我們先來看看KeySet,HashMap中的成員變數keySet儲存了所有的Key集合,事實上,這是繼承自它的父類AbstractMap的成員變數:

transient Set keySet;

而keySet方法,也是覆蓋了父類的方法:

//AbstractMap 中的keySet方法

    public Set keySet() {
        Set ks = keySet;
        if (ks == null) {
            ks = new AbstractSet() {
                public Iterator iterator() {
                    return new Iterator() {
                        private Iterator> i = entrySet().iterator();

                        public boolean hasNext() {
                            return i.hasNext();
                        }

                        public K next() {
                            return i.next().getKey();
                        }

                        public void remove() {
                            i.remove();
                        }
                    };
                }

                public int size() {
                    return AbstractMap.this.size();
                }

                public boolean isEmpty() {
                    return AbstractMap.this.isEmpty();
                }

                public void clear() {
                    AbstractMap.this.clear();
                }

                public boolean contains(Object k) {
                    return AbstractMap.this.containsKey(k);
                }
            };
            keySet = ks;
        }
        return ks;
    }
//HashMap 中的keySet方法

    /**
     * 返回一個鍵值的集合檢視,該集合由map支援,因此對map的更改會反映在集合中,反之亦然。
     * 如果在對集合進行迭代的過程中修改了map中的對映(除了透過迭代器的刪除操作),迭代的結果是未定義的。
     * 該集合支援元素刪除,透過Iterator.remove,Set.remove,removeAll,retainAll和clear操作
     * 從對映中刪除相應的對映。 它不支援add或addAll操作。
     */
    public Set keySet() {
        Set ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

可以看到,AbstractMap中keySet是一個AbstractSet型別,而覆蓋後的keySet方法中,keySet被賦值為KeySet型別。翻翻構造器可以發現,在構造器中並沒有初始化keySet,而是在KeySet方法中對keySet進行的初始化(HashMap中都是使用類似的懶載入機制),KeySet是HashMap中的一個內部類,讓我們再來看看這個KeySet型別的全貌:

    final class KeySet extends AbstractSet {
        public final int size()                 { return size; }
        public final void clear()               { this.clear(); }
        public final Iterator iterator()     { return new KeyIterator(); }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator spliterator() {
            return new KeySpliterator(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer super K> action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i  e = tab[i]; e != null; e = e.next)
                        action.accept(e.key);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

其實KeySet就是繼承自AbstractSet,並覆蓋了其中的大部分方法,遍歷KeySet時,會使用其中的KeyIterator,至於Spliterator,是為並行遍歷設計的,一般是用於Stream的並行操作。forEach方法則是用於遍歷操作,將函式式介面操作action應用於每一個元素,我們來看一個小栗子:

public class Test {

    public static void main(String[] args) {
        Map map = new HashMap();
        map.put("小明", 66);
        map.put("小李", 77);
        map.put("小紅", 88);
        map.put("小剛", 89);
        map.put("小力", 90);
        map.put("小王", 91);
        map.put("小黃", 92);
        map.put("小青", 93);
        map.put("小綠", 94);
        map.put("小黑", 95);
        map.put("小藍", 96);
        map.put("小紫", 97);
        map.put("小橙", 98);
        map.put("小赤", 99);
        map.put("Frank", 100);

        Set ks = map.keySet();
        System.out.printf("keySet:%s,keySet的大小:%d,keySet中是否包含Frank:%s", ks, ks.size(), ks.contains("Frank"));
        System.out.println();
        ks.forEach((item) -> System.out.println(item));
    }
}

輸出如下:

keySet:[小剛, 小橙, 小藍, 小力, 小青, 小黑, 小明, 小李, 小王, 小紫, 小紅, 小綠, Frank, 小黃, 小赤],keySet的大小:15,keySet中是否包含Frank:true
小剛
小橙
小藍
小力
小青
小黑
小明
小李
小王
小紫
小紅
小綠
Frank
小黃
小赤

如果不記得這個AbstractMap和AbstractSet在容器框架中是什麼地位,可以往前翻翻這系列文章的第一篇,看看容器家族的族譜。
但是說了這麼多,這個keySet。裡面的元素是什麼時候放進去的呢?我們自然會想到,大概就是呼叫put方法往裡新增元素的時候,順便把key放進keySet中,完美!讓我們再回顧一下putVal方法,來看看是不是這樣的:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        //如果當前table未初始化,則先重新調整大小至初始容量
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n-1)& hash 這個地方即根據hash求序號,想了解更多雜湊相關內容可以檢視下一篇
        if ((p = tab[i = (n - 1) & hash]) == null)
            //不存在,則新建節點
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            //先找到對應的node
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //如果是樹節點,則呼叫相應的putVal方法,這部分放在第三篇內容裡
                //todo putTreeVal
                e = ((TreeNode)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
                            //如果連結串列長度達到樹化的最大長度,則進行樹化,該函式內容也放在第三篇
                            //todo treeifyBin
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果已存在該key的對映,則將值進行替換
            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;
    }

emmmmm,好像沒找到?你也許會想,會不會是在TreeNode的putTreeVal方法或者在treeifyBin方法中對key進行插入?好了好了,不要再翻了,其實這個奧秘隱藏在KeySet的迭代器中,再回頭看看,它的迭代器返回的是一個KeyIterator,而KeyIterator也是HashMap中的一個內部類,繼承自HashMap中的另一個內部類HashIterator。

HashIterator

讓我們帶著這個疑問,來看看這個HashIterator類裡到底有什麼玄機:

    abstract class HashIterator {
        //指向下一個節點
        Node next;
        //當前節點
        Node current;
        //為實現 fast-fail 機制而設定的期望修改數
        int expectedModCount;
        //當前遍歷到的序號
        int index;

        HashIterator() {
            expectedModCount = modCount;
            Node[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) {
                // 移動到第一個非null節點
                do {} while (index  nextNode() {
            Node[] t;
            Node e = next;
            // fast-fail 機制的實現 即在迭代器往後遍歷時,每次都檢測expectedModCount是否和modCount相等
            // 不相等則丟擲ConcurrentModificationException異常
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //如果遍歷越界,則丟擲NoSuchElementException異常
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                //如果遍歷到末尾,則跳到table中下一個不為null的節點處
                do {} while (index  p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            //移除節點
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

可以發現,在迭代器中,使用nextNode進行遍歷時,先把next引用賦值給current,然後把next.next賦值給next,再獲取了外部類HashMap中的table引用(t = table),這樣就直接透過遍歷table的方式來實現對key,value和entry的讀取。

 if ((next = (current = e).next) == null && (t = table) != null) {
     //如果遍歷到末尾,則跳到table中下一個不為null的節點處
     do {} while (index 

KeyIterator,ValueIterator,EntryIterator都是HashIterator的子類,實現也很簡單,僅僅修改了泛型型別:

    final class KeyIterator extends HashIterator
            implements Iterator {
        public final K next() { return nextNode().key; }
    }

    final class ValueIterator extends HashIterator
            implements Iterator {
        public final V next() { return nextNode().value; }
    }

    final class EntryIterator extends HashIterator
            implements Iterator> {
        public final Map.Entry next() { return nextNode(); }
    }

這樣keySet在遍歷的時候,就可以透過它的迭代器去遍歷訪問外部類HashMap中的table,類似的,values和entrySet也是使用相似的方式進行遍歷。

    public Collection values() {
        Collection vs = values;
        if (vs == null) {
            vs = new Values();
            values = vs;
        }
        return vs;
    }

    final class Values extends AbstractCollection {
        public final int size()                 { return size; }
        public final void clear()               { this.clear(); }
        public final Iterator iterator()     { return new ValueIterator(); }
        public final boolean contains(Object o) { return containsValue(o); }
        public final Spliterator spliterator() {
            return new ValueSpliterator(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer super V> action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i  e = tab[i]; e != null; e = e.next)
                        action.accept(e.value);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }
    public Set> entrySet() {
        Set> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
    }

    final class EntrySet extends AbstractSet> {
        public final int size()                 { return size; }
        public final void clear()               { this.clear(); }
        public final Iterator> iterator() {
            return new EntryIterator();
        }
        public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry,?> e = (Map.Entry,?>) o;
            Object key = e.getKey();
            Node candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }
        public final boolean remove(Object o) {
            if (o instanceof Map.Entry) {
                Map.Entry,?> e = (Map.Entry,?>) o;
                Object key = e.getKey();
                Object value = e.getValue();
                return removeNode(hash(key), key, value, true, true) != null;
            }
            return false;
        }
        public final Spliterator> spliterator() {
            return new EntrySpliterator(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer super Map.Entry> action) {
            Node[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i  e = tab[i]; e != null; e = e.next)
                        action.accept(e);
                }
                if (modCount != mc)
                    throw new ConcurrentModificationException();
            }
        }
    }

至此,這個未解之謎算是告一段落了。

transient

但是,細心的同學可能會發現,HashMap中的table,entrySet,keySet,value等成員變數,都是用transient修飾的,為什麼要這樣做呢?
首先,我們還是先說說這個transient是幹嘛用的,這就要涉及Java中的序列化了,序列化是什麼東西呢?
Java中物件的序列化指的是將物件轉換成以位元組序列的形式來表示,這些位元組序列包含了物件的資料和資訊。
一個序列化後的物件可以被寫到資料庫或檔案中,也可用於網路傳輸,一般當我們使用快取cache(記憶體空間不夠有可能會本地儲存到硬碟)或遠端呼叫rpc(網路傳輸)的時候,
經常需要讓我們的實體類實現Serializable介面,目的就是為了讓其可序列化。
當然,就像資料儲存是為了讀取那樣,序列化後的最終目的是為了恢復成原先的Java物件,要不然序列化後幹嘛呢,這個過程就叫做反序列化。
當我們使用實現Serializable介面的方式來進行序列化時,所有欄位都會被序列化,那如果不想讓某個欄位被序列化(比如出於安全考慮,不將敏感欄位序列化傳輸),便可以使用transient關鍵字來標誌,表示不想讓這個欄位被序列化。
那麼問題來了,儲存節點資訊的table用transient修飾了,那麼序列化和反序列化的時候,資料還怎麼傳輸???
emmmm,這又涉及到一個蛋疼的操作,序列化並沒有那麼簡單,實現了Serializable介面後,在序列化時,會先檢測這個類是否存在writeObject和readObject方法,如果存在,則呼叫相應的方法:

    /**
     * 將HashMap的例項狀態儲存到一個流中
     */
    private void writeObject(java.io.ObjectOutputStream s)
            throws IOException {
        int buckets = capacity();
        // 寫出threshold,loadfactor和所有隱藏的成員
        s.defaultWriteObject();
        s.writeInt(buckets);
        s.writeInt(size);
        internalWriteEntries(s);
    }

    /**
     * 從流中重構HashMap例項
     */
    private void readObject(java.io.ObjectInputStream s)
            throws IOException, ClassNotFoundException {
        // 讀取threshold,loadfactor和所有隱藏的成員
        s.defaultReadObject();
        reinitialize();
        if (loadFactor  0) {
            // (如果是0,則使用預設值)
            // Size the table using given load factor only if within
            // range of 0.25...4.0
            float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
            float fc = (float)mappings / lf + 1.0f;
            int cap = ((fc = MAXIMUM_CAPACITY) ?
                            MAXIMUM_CAPACITY :
                            tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap [] tab = (Node[])new Node[cap];
            table = tab;

            // 讀取鍵值對資訊,然後把對映插入HashMap例項中
            for (int i = 0; i 

這確實是一個極其糟糕的設計。。。而且這裡還是一個private方法。
那麼直接使用預設的序列化不好嗎?非要大費周章的騷操作一波?一部分原因是為了解決效率問題,因為HashMap中很多桶是空的,將其序列化沒有任何意義,所以需要手動使用 writeObject() 方法,只序列化實際儲存元素的陣列。另一個很重要的原因便是,HashMap的儲存是依賴於物件的hashCode的,而Object.hashCode()方法是依賴於具體虛擬機器的,所以同一個物件,在不同虛擬機器中的HashCode可能不同,那這樣對映到的HashMap中的位置也不一樣,這樣序列化和反序列化的物件就不一樣了。引用大神的一段話:

For example, consider the case of a hash table. The physical
representation is a sequence of hash buckets containing key-value
entries. The bucket that an entry resides in is a function of the hash
code of its key, which is not, in general, guaranteed to be the same
from JVM implementation to JVM implementation. In fact, it isn't even
guaranteed to be the same from run to run. Therefore, accepting the
default serialized form for a hash table would constitute a serious
bug. Serializing and deserializing the hash table could yield an
object whose invariants were seriously corrupt.

  蹩腳翻譯一下:

例如,考慮雜湊表的情況。 它的物理儲存是一系列包含鍵值條目的雜湊桶。 條目駐留的儲存區是其金鑰的雜湊碼的函式,
通常,JVM的實現不保證相同。 事實上,它甚至不能保證每次執行都是一樣的。 因此,接受雜湊表的預設序列化形式將構成嚴重的錯誤。 
對雜湊表進行序列化和反序列化可能會產生不變性被嚴重損毀的物件。

好了,到此為止,這部分內容算是over了,後面會繼續介紹HashMap中最麻煩的一部分,TreeNode讓我們師母已呆
記得動動小手點個贊或者點個關注哦,如果覺得不錯的話,也歡迎分享給你的朋友,讓bug傳播的更遠一些,呸,說錯了,讓知識傳播的更遠一些如果寫的有誤的地方,歡迎大家及時指出,我會第一時間予以修正,也歡迎提出改進建議,之後還會繼續更新,歡迎繼續關注!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4422/viewspace-2802881/,如需轉載,請註明出處,否則將追究法律責任。

相關文章