當前位置: 妍妍網 > 碼農

為什麽阿裏不推薦使用 keySet() 遍歷HashMap?

2024-03-20碼農

來源:juejin.cn/post/7295353579002396726

👉 歡迎 ,你將獲得: 專屬的計畫實戰 / Java 學習路線 / 一對一提問 / 學習打卡 / 贈書福利

全棧前後端分離部落格計畫 1.0 版本完結啦,2.0 正在更新中 ... , 演示連結 http://116.62.199.48/ ,全程手摸手,後端 + 前端全棧開發,從 0 到 1 講解每個功能點開發步驟,1v1 答疑,直到計畫上線。 目前已更新了239小節,累計38w+字,講解圖:1645張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將Java領域典型的計畫都整一波,如秒殺系統, 線上商城, IM即時通訊,Spring Cloud Alibaba 等等,

  • Part1 引言

  • Part2 keySet如何遍歷了兩次

  • 1 iterator()

  • 2 HashMap.KeySet#iterator()

  • 3 HashMap.KeyIterator

  • 4 HashMap.HashIterator

  • Part3 總結

  • Part1 引言

    HashMap相信所有學Java的都一定不會感到陌生,作為一個非常重用且非常實用的Java提供的容器,它在我們的程式碼裏面隨處可見。因此遍歷操作也是我們經常會使用到的。HashMap的遍歷方式現如今有非常多種:

    1. 使用叠代器(Iterator)。

    2. 使用 keySet() 獲取鍵的集合,然後透過增強的 for 迴圈遍歷鍵。

    3. 使用 entrySet() 獲取鍵值對的集合,然後透過增強的 for 迴圈遍歷鍵值對。

    4. 使用 Java 8+ 的 Lambda 運算式和流。

    以上遍歷方式的孰優孰劣,在【阿裏巴巴開發手冊】中寫道:

    圖片

    這裏推薦使用的是 entrySet 進行遍歷,在Java8中推薦使用 Map.forEach() 。給出的理由是 遍歷次數 上的不同。

    1. keySet遍歷,需要經過 兩次 遍歷。

    2. entrySet遍歷,只需要 一次 遍歷。

    其中keySet遍歷了兩次,一次是轉為Iterator物件,另一次是從hashMap中取出key所對應的value。

    其中後面一段話很好理解,但是前面這句話卻有點繞,為什麽轉換成了Iterator遍歷了一次?

    我查閱了各個平台對HashMap的遍歷,其中都沒有或者原封不動的照搬上句話。(當然也可能是我沒有查閱到靠譜的文章,歡迎指正)

    Part2 keySet如何遍歷了兩次

    我們首先寫一段程式碼,使用keySet遍歷Map。

    public class Test {
    public static void main(String[] args) {
    Map<String, String> map = new HashMap<>();
    map.put("k1""v1");
    map.put("k2""v2");
    map.put("k3""v3");
    for (String key : map.keySet()) {
    String value = map.get(key);
    System.out.println(key + ":" + value);
    }
    }
    }

    執行結果顯而易見的是

    k1:v1
    k2:v2
    k3:v3

    兩次遍歷,第一次遍歷所描述的是轉為Iterator物件我們好像沒有從程式碼中看見,我們看到的後面所描述的遍歷,也就是遍歷 map,keySet() 所返回的 Set 集合中的key,然後去HashMap中拿取value的。

    Iterator物件呢?如何遍歷轉換為Iterator物件的呢?

    圖片

    首先我們這種遍歷方式大家都應該知道是叫: 增強for迴圈,for-each

    這是一種Java的語法糖~。

    我們可以透過反編譯,或者直接透過Idea在 class檔中檢視對應的 class檔

    圖片

    public class Test {
    public Test() {
    }
    public static void main(String[] args) {
    Map<String, String> map = new HashMap();
    map.put("k1""v1");
    map.put("k2""v2");
    map.put("k3""v3");
    Iterator var2 = map.keySet().iterator();
    while(var2.hasNext()) {
    String key = (String)var2.next();
    String value = (String)map.get(key);
    System.out.println(key + ":" + value);
    }
    }
    }

    和我們編寫的是存在差異的,其中我們可以看到其中透過 map.keySet().iterator() 獲取到了我們所需要看見的 Iterator 物件。

    那麽它又是怎麽轉換成的呢?為什麽需要遍歷呢?我們檢視 iterator() 方法

    1 iterator()

    圖片

    發現是Set定義的一個介面。返回此集合中元素的叠代器

    2 HashMap.KeySet#iterator()

    我們檢視HashMap中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, falsetrue) != 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();
    }
    }
    }

    其中的iterator()方法返回的是一個 KeyIterator 物件,那麽究竟是在哪裏進行了遍歷呢?我們接著往下看去。

    3 HashMap.KeyIterator

    圖片

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

    這個類也很簡單:

    1. 繼承了 HashIterator 類。

    2. 實作了 Iterator 介面。

    3. 一個 next() 方法。

    還是沒有看見哪裏進行了遍歷,那麽我們繼續檢視 HashIterator

    4 HashMap.HashIterator

    圖片

    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, falsefalse);
    expectedModCount = modCount;
    }
    }


    我們可以發現這個構造器中存在了一個 do-while 迴圈操作,目的是找到一個第一個不為空的 entry

    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);
    }
    }

    KeyIterator 是extend HashIterator 物件的。這裏涉及到了繼承的相關概念,大家忘記的可以找相關的文章看看,或者我也可以寫一篇~~dog。

    例如兩個類

    public class Father {
    public Father(){
    System.out.println("father");
    }
    }
    public class Son extends Father{
    public static void main(String[] args) {
    Son son = new Son();
    }
    }

    建立Son物件的同時,會執行Father構造器。也就會打印出 father 這句話。

    那麽這個迴圈操作就是我們要找的迴圈操作了。

    Part3 總結

    1. 使用keySet遍歷,其實內部是使用了對應的 iterator() 方法。

    2. iterator() 方法是建立了一個 KeyIterator 物件。

    3. KeyIterator 物件extend HashIterator 物件。

    4. HashIterator 物件的構造方法中,會遍歷找到第一個不為空的 entry

    keySet->iterator()->KeyIterator->HashIterator

    👉 歡迎 ,你將獲得: 專屬的計畫實戰 / Java 學習路線 / 一對一提問 / 學習打卡 / 贈書福利

    全棧前後端分離部落格計畫 1.0 版本完結啦,2.0 正在更新中 ... , 演示連結 http://116.62.199.48/ ,全程手摸手,後端 + 前端全棧開發,從 0 到 1 講解每個功能點開發步驟,1v1 答疑,直到計畫上線。 目前已更新了239小節,累計38w+字,講解圖:1645張,還在持續爆肝中.. 後續還會上新更多計畫,目標是將Java領域典型的計畫都整一波,如秒殺系統, 線上商城, IM即時通訊,Spring Cloud Alibaba 等等,


    1. 

    2. 

    3. 

    4. 

    最近面試BAT,整理一份面試資料Java面試BATJ通關手冊,覆蓋了Java核心技術、JVM、Java並行、SSM、微服務、資料庫、數據結構等等。

    獲取方式:點「在看」,關註公眾號並回復 Java 領取,更多內容陸續奉上。

    PS:因公眾號平台更改了推播規則,如果不想錯過內容,記得讀完點一下在看,加個星標,這樣每次新文章推播才會第一時間出現在你的訂閱列表裏。

    「在看」支持小哈呀,謝謝啦