1. 程式人生 > >原始碼解析HashMap的keySet()方法

原始碼解析HashMap的keySet()方法

HashMap的keySet()方法比較簡單,作用是獲取HashMap中的key的集合。雖然這個方法十分簡單,似乎沒有什麼可供分析的,但真正看了原始碼,發現自己還是有很多不懂的地方。下面是keySet的程式碼。

    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new KeySet();
            keySet = ks;
        }
        return ks;
    }

從程式碼中瞭解到,第一次呼叫keySet方法時,keySet屬性是null,然後進行了初始化,再將keySet屬性返回。也就是說,HashMap裡並不會隨著put和remove的進行也維護一個keySet集合,而是在第一次呼叫keySet方法時,才給keySet屬性初始化。

按照自己以往的理解,以為keySet返回的是一個集合,集合裡面儲存了HashMap的所有的Key。因為有了中先入為主的印象,所以讀原始碼時,才感覺原始碼很奇怪。從原始碼中可以看到,初始化時,只是建立了一個KeySet類的物件,並沒有把HashMap的key都加入進來,方法就返回了。除了自己以往的理解外,還有一個現象,讓我堅信這時HashMap的key已經加入到keySet了,那就是在除錯程式碼過程中IDE給出的除錯資訊。如下圖。從圖中可以看出,建立完成KeySet()後,除錯資訊就已經可以顯示出,ks中有2個元素了。這個資訊更加堅定了自己之前的理解。

那麼,HashMap的key是什麼時候加入到keySet集合中的呢?順著這個思路,我進行了一步一步的分析。自己看了KeySet類的建構函式,發現只有預設建構函式。那麼我想,如果沒有在KeySet建構函式裡把HashMap的key加入進來,那麼就有可能是在KeySet的父類的建構函式中加入進來的。然後,自己找遍了KeySet類的父類的建構函式,發現都是空實現,並沒有任何加入HashMap的key的操作。這到底是怎麼回事呢?

其實HashMap的key並沒有加入到keySet集合中,而是在遍歷的時候,使用迭代器對key進行的遍歷。這是結論。下面我們看一下原因和過程。

首先看一下KeySet類的程式碼,如下圖。可以看到,KeySet類中的迭代器函式,返回的是一個KeyIterator類的物件。它的next方法返回的是HashIterator的nextNode的key。也就是說,當使用迭代器遍歷set內的元素時,KeySet類的迭代器,會保證能夠依次獲取到HashMap的節點的key值,這就是我們遍歷keySet的過程的實質。

    final class KeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { HashMap.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();
            }
        }
    }
    final class KeyIterator extends HashIterator
        implements Iterator<K> {
        public final K next() { return nextNode().key; }
    }
    abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                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;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = 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;
        }
    }

那麼,這裡我們可以思考這麼一個問題。通過HashMap的keySet獲取到keySet後,難道只能用迭代器遍歷嗎?keySet方法不把HashMap的key都加入到set中,那麼呼叫者使用for(int i = 0; i < size; i ++)的方式遍歷時,豈不是無法遍歷set中的key了嗎?是的,確實是的。keySet確實沒有把key加入到set中,另外,它不用擔心呼叫者用for(int i = 0; i < size; i ++)的方式遍歷時獲取不到key,因為set根本就沒有set.get(i)這樣類似的方法,要想遍歷set,只能用迭代器,或者使用foreach方式(本質還是迭代器)。

這裡還有個問題需要解釋,就是在除錯程式碼時,既然key沒有加入到set中,那麼IDE如何顯示出set中有2個元素這樣的資訊的?原來,IDE顯示物件資訊時,會呼叫物件的toString方法。而集合的toString方法就是顯示出集合中的元素個數。

這裡再思考一步,如果我們在集合的toString方法加上斷點,那麼IDE顯示物件資訊時,會不先停下來?答案是看情況。記得早些年間使用eclipse除錯程式碼時,在toString方法加上斷點後,顯示物件資訊時確實會停下來。然而我現在使用的是IDE是idea,idea在這一點上做了優化。如果是IDE顯示物件資訊呼叫的toString方法,那麼toString方法的斷點會被跳過,即不生效,但會給出一條提示資訊,如下圖。如果程式設計師主動呼叫物件的toString方法,那麼,toString方法的斷點會生效,可以正常斷點除錯。