面試官:爲什麼阿里不推薦使用 keySet() 遍歷 HashMap?太叼鑽了吧。。

來源:https://juejin.cn/post/7295353579002396726

Part1 引言

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

  1. 使用迭代器(Iterator)。
  2. 使用 keySet() 獲取鍵的集合,然後通過增強的 for 循環遍歷鍵。
  3. 使用 entrySet() 獲取鍵值對的集合,然後通過增強的 for 循環遍歷鍵值對。
  4. 使用 Java 8+ 的 Lambda 表達式和流。

以上遍歷方式的孰優孰劣,在《阿里巴巴開發手冊》中寫道:

這裏推薦使用的是entrySet進行遍歷,在Java8中推薦使用Map.forEach()。給出的理由是遍歷次數上的不同。獲取這份最新完整版手冊,請在公衆號 "Java核心技術" 回覆"手冊"即可免費獲取。

  1. keySet遍歷,需要經過兩次遍歷。
  2. entrySet遍歷,只需要一次遍歷。

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

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

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

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

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

其中的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, false, false);
        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是extendHashIterator對象的。這裏涉及到了繼承的相關概念,大家忘記的可以找相關的文章看看,或者我也可以寫一篇~~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對象extendHashIterator對象。
  4. HashIterator對象的構造方法中,會遍歷找到第一個不爲空的entry

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

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這纔是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章