java的容器類,實現
Collection
接口的都會實現迭代器方式,Map則有點特殊,它不實現Collection
接口,它的迭代使用方式則主要藉助Collection
來實現java
對於List
,Set
,咱們能夠直接用 foreach
來實現遍歷,而Map
則不能直接這麼用,一般Map的遍歷方式有三種數組
for(Map.Entry entry: map.entrySet()) { // xxx }
for(Object key : map.keySet()) { // xxx }
for(Object value: map.values()) { // xxxx }
上面遍歷主要依賴的三個方法,前兩個返回的都是Set
,那麼就有下面幾個問題ide
map.entrySet
返回的Entry集合元素個數和Map的size是否相同map.keySet
對於key的hashcode相同的場景會出現什麼狀況map.values
Map中value沒有校驗,所以value集合容量應能夠小於map.size()
entrySet
方法的實現以下:學習
public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
能夠看到返回的是內部成員變量 entrySet
,問題就集中在這個成員變量是如何維護的測試
按正常的理解是,在添加刪除元素的時候,同步維護entrySet
的值算是最簡單的方法了,然而前面博文《JDK容器學習之HashMap (二) : 讀寫邏輯詳解》中,並無看到有維護這一段的邏輯this
掃了一遍代碼,愣是沒有發如今什麼地方維護有顯示的向Set中添加or移除元素了.net
惟一的可能性就是下面這個初始化了,這一行代碼到底作了什麼呢?code
entrySet = new EntrySet();
這裏就只是建立了一個對象,接下來則須要研究下這個對象是個什麼鬼了對象
final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public final int size() { return size; } public final void clear() { HashMap.this.clear(); } public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator(); } public final boolean contains(Object o) { // xxx } public final boolean remove(Object o) { // xxx } public final Spliterator<Map.Entry<K,V>> spliterator() { return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super Map.Entry<K,V>> action) { // xxx } } final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } }
首先 EntrySet
是一個 Set
對象,而Set
的遍歷採用迭代器模式,迭代器模式主要依賴的 iterator()
方法的實現blog
返回繼承 hashIterator
的 EntryIterator
對象,其中的核心的next()
方法就是調用的 hashIterator.nextNode()
到這裏,就能夠大膽的得出結論,遍歷 entrySet
其實就是在依次調用 hashIterator.nextNode()
方法,這個Set自己是不作元素的添加移除操做的,它就是直接封裝了的HashMap內部的HashIterator
,對外提供服務
HashIterator
hash迭代器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) { // 遍歷數組直到找到第一個非空的Node節點 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) { // 若出現hash碰撞,且當前節點的鏈尾非空,則next指向鏈表下一個節點 // 沒有hash碰撞,or鏈表尾爲空,即Node節點內部的next指向空 // 繼續掃描table數組,找到下一個有效的Node節點,並賦值給next 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; } }
遍歷的邏輯以下:
初始化:掃描table數組,找到第一個有效的Node對象並賦值給next對象
依次遍歷:
將next對象賦值給臨時變量e
從新設置next對象
因此,針對數組+鏈表的結構圖,掃描的流程應該是
問題一
map.entrySet
返回的Entry集合元素個數和Map的size是否相同
entrySet
集合實際上持有的依然是table數組中的數據對象,其迭代器就是掃描的table數組,因此size應該相同借用上次的測試case進行實測, 下面的Demo從新hashCode
確保會出現碰撞
public static class Demo { public int num; public Demo(int num) { this.num = num; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Demo demo = (Demo) o; return num == demo.num; } @Override public int hashCode() { return num % 3 + 16; } } @Test public void testMapResize() { Map<Demo, Integer> map = new HashMap<>(); for(int i = 1; i < 12; i++) { map.put(new Demo(i), i); } Set<Map.Entry<Demo, Integer>> set = map.entrySet(); System.out.println(set.size()); // 11 Assert.assertTrue(set.size() == map.size()); }
keySet
, values
實際上三個實現思路差很少,都是定義一個內部Set對象,迭代器實現對table數組的掃描,由於原理大同小異,再也不進行贅述, 看下面兩個迭代器基本就知道了
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; } }
一樣回答下上面的問題
** map.keySet
對於key的hashcode相同的場景會出現什麼狀況**
equals
方法來去重的,與hashcode
關係不太大map.values
Map中value沒有校驗,所以value集合容量應能夠小於map.size()
根據不一樣的場景選擇遍歷方式
EntrySet
KeySet
ValueSet
上面的遍歷實現,很是的有意思,也有不小的借鑑意義,好比但願給一個對象的內部元素提供一些特殊的遍歷方式,能夠參考一下這種作法
實現思路:
next
方法實現成員變量的迭代邏輯掃一掃二維碼,關注 小灰灰blog