集合詳解之 Map + 面試題

集合詳解之 Map + 面試題

集合有兩個大接口:Collection 和 Map,本文重點來說解集合中另外一個經常使用的集合類型 Map。java

如下是 Map 的繼承關係圖:面試

Map 簡介

Map 經常使用的實現類以下:數組

  • Hashtable:Java 早期提供的一個哈希表實現,它是線程安全的,不支持 null 鍵和值,由於它的性能不如 ConcurrentHashMap,因此不多被推薦使用。
  • HashMap:最經常使用的哈希表實現,若是程序中沒有多線程的需求,HashMap 是一個很好的選擇,支持 null 鍵和值,若是在多線程中可用 ConcurrentHashMap 替代。
  • TreeMap:基於紅黑樹的一種提供順序訪問的 Map,自身實現了 key 的天然排序,也能夠指定 Comparator 來自定義排序。
  • LinkedHashMap:HashMap 的一個子類,保存了記錄的插入順序,可在遍歷時保持與插入同樣的順序。

Map 經常使用方法

經常使用方法包括:put、remove、get、size 等,全部方法以下圖:安全

使用示例,請參考如下代碼:數據結構

Map hashMap = new HashMap();
// 增長元素
hashMap.put("name", "老王");
hashMap.put("age", "30");
hashMap.put("sex", "你猜");
// 刪除元素
hashMap.remove("age");
// 查找單個元素
System.out.println(hashMap.get("age"));
// 循環全部的 key
for (Object k : hashMap.keySet()) {
    System.out.println(k);
}
// 循環全部的值
for (Object v : hashMap.values()) {
    System.out.println(v);
}

以上爲 HashMap 的使用示例,其餘類的使用也是相似。多線程

HashMap 數據結構

HashMap 底層的數據是數組被成爲哈希桶,每一個桶存放的是鏈表,鏈表中的每一個節點,就是 HashMap 中的每一個元素。在 JDK 8 當鏈表長度大於等於 8 時,就會轉成紅黑樹的數據結構,以提高查詢和插入的效率。併發

HashMap 數據結構,以下圖:app

1)添加方法:put(Object key, Object value) 函數

執行流程以下:性能

  • 對 key 進行 hash 操做,計算存儲 index;
  • 判斷是否有哈希碰撞,若是沒碰撞直接放到哈希桶裏,若是有碰撞則以鏈表的形式存儲;
  • 判斷已有元素的類型,決定是追加樹仍是追加鏈表,當鏈表大於等於 8 時,把鏈表轉換成紅黑樹;
  • 若是節點已經存在就替換舊值;
  • 判斷是否超過閥值,若是超過就要擴容。

源碼及說明:

public V put(K key, V value) {
    // 對 key 進行 hash()
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
  // 對 key 進行 hash() 的具體實現
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // tab爲空則建立
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 計算 index,並對 null 作處理
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 節點存在
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 該鏈爲樹
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 該鏈爲鏈表
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 寫入
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 超過load factor\*current capacity,resize
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

put() 執行流程圖以下:

* 首先比對首節點,若是首節點的 hash 值和 key 的 hash 值相同,而且首節點的鍵對象和 key 相同(地址相同或 equals 相等),則返回該節點;

  • 若是首節點比對不相同、那麼看看是否存在下一個節點,若是存在的話,能夠繼續比對,若是不存在就意味着 key 沒有匹配的鍵值對。

源碼及說明:

public V get(Object key) {
  Node<K,V> e;
  return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/\*\* \* 該方法是 Map.get 方法的具體實現 \* 接收兩個參數 \* @param hash key 的 hash 值,根據 hash 值在節點數組中尋址,該 hash 值是經過 hash(key) 獲得的 \* @param key key 對象,當存在 hash 碰撞時,要逐個比對是否相等 \* @return 查找到則返回鍵值對節點對象,不然返回 null \*/
final Node\<K,V\> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k; // 聲明節點數組對象、鏈表的第一個節點對象、循環遍歷時的當前節點對象、數組長度、節點的鍵對象
    // 節點數組賦值、數組長度賦值、經過位運算獲得求模結果肯定鏈表的首節點
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // 首先比對首節點,若是首節點的 hash 值和 key 的 hash 值相同,而且首節點的鍵對象和 key 相同(地址相同或 equals 相等),則返回該節點
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first; // 返回首節點

        // 若是首節點比對不相同、那麼看看是否存在下一個節點,若是存在的話,能夠繼續比對,若是不存在就意味着 key 沒有匹配的鍵值對 
        if ((e = first.next) != null) {
            // 若是存在下一個節點 e,那麼先看看這個首節點是不是個樹節點
            if (first instanceof TreeNode)
                // 若是是首節點是樹節點,那麼遍歷樹來查找
                return ((TreeNode<K,V>)first).getTreeNode(hash, key); 

            // 若是首節點不是樹節點,就說明仍是個普通的鏈表,那麼逐個遍歷比對便可 
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) // 比對時仍是先看 hash 值是否相同、再看地址或 equals
                    return e; // 若是當前節點e的鍵對象和key相同,那麼返回 e
            } while ((e = e.next) != null); // 看看是否還有下一個節點,若是有,繼續下一輪比對,不然跳出循環
        }
    }
    return null; // 在比對完了應該比對的樹節點 或者所有的鏈表節點 都沒能匹配到 key,那麼就返回 null

相關面試題

1.Map 常見實現類有哪些?

答:Map 的常見實現類以下列表:

  • Hashtable:Java 早期提供的一個哈希表實現,它是線程安全的,不支持 null 鍵和值,由於它的性能不如 ConcurrentHashMap,因此不多被推薦使用;
  • HashMap:最經常使用的哈希表實現,若是程序中沒有多線程的需求,HashMap 是一個很好的選擇,支持 null 鍵和值,若是在多線程中可用 ConcurrentHashMap 替代;
  • TreeMap:基於紅黑樹的一種提供順序訪問的 Map,自身實現了 key 的天然排序,也能夠指定的 Comparator 來自定義排序;
  • LinkedHashMap:HashMap 的一個子類,保存了記錄的插入順序,可在遍歷時保持與插入同樣的順序。

2.使用 HashMap 可能會遇到什麼問題?如何避免?

答:HashMap 在併發場景中可能出現死循環的問題,這是由於 HashMap 在擴容的時候會對鏈表進行一次倒序處理,假設兩個線程同時執行擴容操做,第一個線程正在執行 B→A 的時候,第二個線程又執行了 A→B ,這個時候就會出現 B→A→B 的問題,形成死循環。
解決的方法:升級 JDK 版本,在 JDK 8 以後擴容不會再進行倒序,所以死循環的問題獲得了極大的改善,但這不是終極的方案,由於 HashMap 原本就不是用在多線程版本下的,若是是多線程可以使用 ConcurrentHashMap 替代 HashMap。

3.如下說法正確的是?

A:Hashtable 和 HashMap 都是非線程安全的
B:ConcurrentHashMap 容許 null 做爲 key
C:HashMap 容許 null 做爲 key
D:Hashtable 容許 null 做爲 key
答:C
題目解析:Hashtable 是線程安全的,ConcurrentHashMap 和 Hashtable 是不容許 null 做爲鍵和值的。

4.TreeMap 怎麼實現根據 value 值倒序?

答:使用 Collections.sort(list, new Comparator<Map.Entry<String, String>>() 自定義比較器實現,先把 TreeMap 轉換爲 ArrayList,在使用 Collections.sort() 根據 value 進行倒序,完整的實現代碼以下。

TreeMap<String, String> treeMap = new TreeMap();
treeMap.put("dog", "dog");
treeMap.put("camel", "camel");
treeMap.put("cat", "cat");
treeMap.put("ant", "ant");
// map.entrySet() 轉成 List
List<Map.Entry<String, String>> list = new ArrayList<>(treeMap.entrySet());
// 經過比較器實現比較排序
Collections.sort(list, new Comparator<Map.Entry<String, String>>() {
  public int compare(Map.Entry<String, String> m1, Map.Entry<String, String> m2) {
    return m2.getValue().compareTo(m1.getValue());
  }
});
// 打印結果
for (Map.Entry<String, String> item : list) {
  System.out.println(item.getKey() + ":" + item.getValue());
}

程序執行結果:

dog:dog
cat:cat
camel:camel
ant:ant

5.如下哪一個 Set 實現了自動排序?

A:LinedHashSet
B:HashSet
C:TreeSet
D:AbstractSet

答:C

6.如下程序運行的結果是什麼?

Hashtable hashtable = new Hashtable();
hashtable.put("table", null);
System.out.println(hashtable.get("table"));

答:程序執行報錯:java.lang.NullPointerException。Hashtable 不容許 null 鍵和值。

7.HashMap 有哪些重要的參數?用途分別是什麼?

答:HashMap 有兩個重要的參數:容量(Capacity)和負載因子(LoadFactor)。

  • 容量(Capacity):是指 HashMap 中桶的數量,默認的初始值爲 16。
  • 負載因子(LoadFactor):也被稱爲裝載因子,LoadFactor 是用來斷定 HashMap 是否擴容的依據,默認值爲 0.75f,裝載因子的計算公式 = HashMap 存放的 KV 總和(size)/ Capacity。

8.HashMap 和 Hashtable 有什麼區別?

答:HashMap 和 Hashtable 區別以下:

  • Hashtable 使用了 synchronized 關鍵字來保障線程安全,而 HashMap 是非線程安全的;
  • HashMap 容許 K/V 都爲 null,而 Hashtable K/V 都不容許 null;
  • HashMap 繼承自 AbstractMap 類;而 Hashtable 繼承自 Dictionary 類。

9.什麼是哈希衝突?

答:當輸入兩個不一樣值,根據同一散列函數計算出相同的散列值的現象,咱們就把它叫作碰撞(哈希碰撞)。

10.有哪些方法能夠解決哈希衝突?

答:哈希衝突的經常使用解決方案有如下 4 種。

  • 開放定址法:當關鍵字的哈希地址 p=H(key)出現衝突時,以 p 爲基礎,產生另外一個哈希地址 p1,若是 p1 仍然衝突,再以 p 爲基礎,產生另外一個哈希地址 p2,循環此過程直到找出一個不衝突的哈希地址,將相應元素存入其中。
  • 再哈希法:這種方法是同時構造多個不一樣的哈希函數,當哈希地址 Hi=RH1(key)發生衝突時,再計算 Hi=RH2(key),循環此過程直到找到一個不衝突的哈希地址,這種方法惟一的缺點就是增長了計算時間。
  • 鏈地址法:這種方法的基本思想是將全部哈希地址爲 i 的元素構成一個稱爲同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第 i 個單元中,於是查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於常常進行插入和刪除的狀況。
  • 創建公共溢出區:將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一概填入溢出表。

11.HashMap 使用哪一種方法來解決哈希衝突(哈希碰撞)?

答:HashMap 使用鏈表和紅黑樹來解決哈希衝突,詳見本文 put() 方法的執行過程。

12.HashMap 的擴容爲何是 2^n ?

答:這樣作的目的是爲了讓散列更加均勻,從而減小哈希碰撞,以提供代碼的執行效率。

13.有哈希衝突的狀況下 HashMap 如何取值?

答:若是有哈希衝突,HashMap 會循環鏈表中的每項 key 進行 equals 對比,返回對應的元素。相關源碼以下:

do {
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k)))) // 比對時仍是先看 hash 值是否相同、再看地址或 equals
        return e; // 若是當前節點 e 的鍵對象和 key 相同,那麼返回 e
} while ((e = e.next) != null); // 看看是否還有下一個節點,若是有,繼續下一輪比對,不然跳出循環

14.如下程序會輸出什麼結果?

class Person {
    private Integer age;
    public boolean equals(Object o) {
        if (o == null || !(o instanceof Person)) {
            return false;
        } else {
            return this.getAge().equals(((Person) o).getAge());
        }
    }
    public int hashCode() {
        return age.hashCode();
    }
    public Person(int age) {
        this.age = age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Integer getAge() {
        return age;
    }
    public static void main(String[] args) {
        HashMap<Person, Integer> hashMap = new HashMap<>();
        Person person = new Person(18);
        hashMap.put(person, 1);
        System.out.println(hashMap.get(new Person(18)));
    }
}

答:1
題目解析:由於 Person 重寫了 equals 和 hashCode 方法,全部 person 對象和 new Person(18) 的鍵值相同,因此結果就是 1。

15.爲何重寫 equals() 時必定要重寫 hashCode()?

答:由於 Java 規定,若是兩個對象 equals 比較相等(結果爲 true),那麼調用 hashCode 也必須相等。若是重寫了 equals() 但沒有重寫 hashCode(),就會與規定相違背,好比如下代碼(故意註釋掉 hashCode 方法):

class Person {
    private Integer age;
    public boolean equals(Object o) {
        if (o == null || !(o instanceof Person)) {
            return false;
        } else {
            return this.getAge().equals(((Person) o).getAge());
        }
    }
// public int hashCode() {
// return age.hashCode();
// }
    public Person(int age) {
        this.age = age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Integer getAge() {
        return age;
    }
    public static void main(String[] args) {
        Person p1 = new Person(18);
        Person p2 = new Person(18);
        System.out.println(p1.equals(p2));
        System.out.println(p1.hashCode() + " : " + p2.hashCode());
    }
}

執行的結果:

true
21685669 : 2133927002

若是重寫 hashCode() 以後,執行的結果是:

true
18 : 18

這樣就符合了 Java 的規定,所以重寫 equals() 時必定要重寫 hashCode()。

16.HashMap 在 JDK 7 多線程中使用會致使什麼問題?

答:HashMap 在 JDK 7 中會致使死循環的問題。由於在 JDK 7 中,多線程進行 HashMap 擴容時會致使鏈表的循環引用,這個時候使用 get() 獲取元素時就會致使死循環,形成 CPU 100% 的狀況。

17.HashMap 在 JDK 7 和 JDK 8 中有哪些不一樣?

答:HashMap 在 JDK 7 和 JDK 8 的主要區別以下。

  • 存儲結構:JDK 7 使用的是數組 + 鏈表;JDK 8 使用的是數組 + 鏈表 + 紅黑樹。
  • 存放數據的規則:JDK 7 無衝突時,存放數組;衝突時,存放鏈表;JDK 8 在沒有衝突的狀況下直接存放數組,有衝突時,當鏈表長度小於 8 時,存放在單鏈表結構中,當鏈表長度大於 8 時,樹化並存放至紅黑樹的數據結構中。
  • 插入數據方式:JDK 7 使用的是頭插法(先將原位置的數據移到後 1 位,再插入數據到該位置);JDK 8 使用的是尾插法(直接插入到鏈表尾部/紅黑樹)。

總結

經過本文能夠了解到:

  • Map 的經常使用實現類 Hashtable 是 Java 早期的線程安全的哈希表實現;
  • HashMap 是最經常使用的哈希表實現,但它是非線程安全的,可以使用 ConcurrentHashMap 替代;
  • TreeMap 是基於紅黑樹的一種提供順序訪問的哈希表實現;
  • LinkedHashMap 是 HashMap 的一個子類,保存了記錄的插入順序,可在遍歷時保持與插入同樣的順序。

HashMap 在 JDK 7 可能在擴容時會致使鏈表的循環引用而形成 CPU 100%,HashMap 在 JDK 8 時數據結構變動爲:數組 + 鏈表 + 紅黑樹的存儲方式,在沒有衝突的狀況下直接存放數組,有衝突,當鏈表長度小於 8 時,存放在單鏈表結構中,當鏈表長度大於 8 時,樹化並存放至紅黑樹的數據結構中。

_

歡迎關注個人公衆號,回覆關鍵字「Java」 ,將會有大禮相送!!! 祝各位面試成功!!!

相關文章
相關標籤/搜索