JDK容器學習之HashMap (三) : 迭代器實現

HashMap 迭代器實現方式

java的容器類,實現Collection接口的都會實現迭代器方式,Map則有點特殊,它不實現Collection接口,它的迭代使用方式則主要藉助Collection來實現java

1. Map的遍歷方式

對於List,Set,咱們能夠直接用 foreach 來實現遍歷,而Map則不能直接這麼用,一般Map的遍歷方式有三種數組

  1. Entry的遍歷
for(Map.Entry entry: map.entrySet()) {
  // xxx
}
  1. Key的遍歷
for(Object key : map.keySet()) {
  // xxx
}
  1. Value的遍歷
for(Object value: map.values()) {
  // xxxx
}

上面遍歷主要依賴的三個方法,前兩個返回的都是Set,那麼就有下面幾個問題ide

  1. map.entrySet 返回的Entry集合元素個數和Map的size是否相同
  • 簡單來說就是假設有兩個Entry的key的hash值相同
  • 那麼這兩個Entry都會放在這個Set集合中麼?
  • 或者說等同HashMap的數組鏈表格式,Set集合中放的是鏈表頭?
  1. map.keySet 對於key的hashcode相同的場景會出現什麼狀況
  2. map.values Map中value沒有校驗,所以value集合容量應能夠小於map.size()

2. 實現方式

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

返回繼承 hashIteratorEntryIterator 對象,其中的核心的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對象
      • 爲了保證遍歷的可持續性,須要在返回以前,從新獲取到下一個next對象
    • 從新設置next對象

      • 若e的next對象存在(即hash碰撞,且鏈表的下一個節點存在),則next指向下一個節點
      • 若e的next對象爲空
      • 若e沒有後綴(即這個不存在hash碰撞,鏈表結構只有這個鏈頭)
      • 上面兩種狀況,則繼續遍歷table數組,找到下一個有效的Node對象

因此,針對數組+鏈表的結構圖,掃描的流程應該是

arch

問題一

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相同的場景會出現什麼狀況**

  • 沒什麼關係,set中是根據 equals方法來去重的,與hashcode關係不太大

map.values Map中value沒有校驗,所以value集合容量應能夠小於map.size()

  • 不對,經過上面的實現,能夠知道size依然相同

2,小結&收穫

1. 幾種遍歷方式對比

根據不一樣的場景選擇遍歷方式

  • 若是須要kv,則遍歷EntrySet
  • 若是隻須要key, 則遍歷 KeySet
  • 若是隻須要value,則遍歷 ValueSet

2. 有意思的遍歷思路

上面的遍歷實現,很是的有意思,也有不小的借鑑意義,好比但願給一個對象的內部元素提供一些特殊的遍歷方式,能夠參考一下這種作法

實現思路:

  • 內部類實現迭代器
  • next方法實現成員變量的迭代邏輯

3, 相關博文

關注更多

掃一掃二維碼,關注 小灰灰blog

https://static.oschina.net/uploads/img/201709/22221611_Fdo5.jpg

相關文章
相關標籤/搜索