【Java入門提高篇】Day24 Java容器類詳解(七)HashMap源碼分析(下)


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

KeySet

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

transient Set<K> keySet;

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

//AbstractMap 中的keySet方法

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new AbstractSet<K>() {
                public Iterator<K> iterator() {
                    return new Iterator<K>() {
                        private Iterator<Entry<K,V>> 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<K> keySet() { Set<K> 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<K> {
        public final int size()                 { return size; }
        public final void clear()               { this.clear(); }
        public final Iterator<K> 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<K> spliterator() {
            return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super K> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> 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<String, Integer> 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<String> 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<K,V>[] tab; Node<K,V> 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<K,V> 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<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
                            //如果鏈表長度達到樹化的最大長度,則進行樹化,該函數內容也放在第三篇
                            //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<K,V> next;
        //當前節點
        Node<K,V> current;
        //為實現 fast-fail 機制而設置的期望修改數
        int expectedModCount;
        //當前遍歷到的序號
        int index;

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) {
                // 移動到第一個非null節點
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {
            return next != null;
        }

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> 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 < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> 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 < t.length && (next = t[index++]) == null);
}

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

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

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

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

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

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

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

    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final int size()                 { return size; }
        public final void clear()               { this.clear(); }
        public final Iterator<Map.Entry<K,V>> 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<K,V> 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<Map.Entry<K,V>> spliterator() {
            return new EntrySpliterator<K,V>(HashMap.this, 0, -1, 0, 0);
        }
        public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
            Node<K,V>[] tab;
            if (action == null)
                throw new NullPointerException();
            if (size > 0 && (tab = table) != null) {
                int mc = modCount;
                for (int i = 0; i < tab.length; ++i) {
                    for (Node<K,V> 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 || Float.isNaN(loadFactor))
            throw new InvalidObjectException("Illegal load factor: " +
                    loadFactor);
        // 讀取並忽略桶的數量
        s.readInt();
        // 讀取映射的數量
        int mappings = s.readInt();
        if (mappings < 0)
            throw new InvalidObjectException("Illegal mappings count: " +
                    mappings);
        else if (mappings > 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 < DEFAULT_INITIAL_CAPACITY) ?
                    DEFAULT_INITIAL_CAPACITY :
                    (fc >= MAXIMUM_CAPACITY) ?
                            MAXIMUM_CAPACITY :
                            tableSizeFor((int)fc));
            float ft = (float)cap * lf;
            threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
                    (int)ft : Integer.MAX_VALUE);
            
            SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
            table = tab;

            // 讀取鍵值對信息,然后把映射插入HashMap實例中
            for (int i = 0; i < mappings; i++) {
                @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
                @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
                putVal(hash(key), key, value, false, false);
            }
        }
    }

  這確實是一個極其糟糕的設計。。。而且這里還是一個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傳播的更遠一些,呸,說錯了,讓知識傳播的更遠一些如果寫的有誤的地方,歡迎大家及時指出,我會第一時間予以修正,也歡迎提出改進建議,之后還會繼續更新,歡迎繼續關注!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM