集合有兩個大接口:Collection 和 Map,本文重點來說解集合中另外一個經常使用的集合類型 Map。java
如下是 Map 的繼承關係圖:面試
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 中的每一個元素。在 JDK 8 當鏈表長度大於等於 8 時,就會轉成紅黑樹的數據結構,以提高查詢和插入的效率。併發
HashMap 數據結構,以下圖:app
1)添加方法:put(Object key, Object value) 函數
執行流程以下:性能
源碼及說明:
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() 執行流程圖以下:
源碼及說明:
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
答:Map 的常見實現類以下列表:
答:HashMap 在併發場景中可能出現死循環的問題,這是由於 HashMap 在擴容的時候會對鏈表進行一次倒序處理,假設兩個線程同時執行擴容操做,第一個線程正在執行 B→A 的時候,第二個線程又執行了 A→B ,這個時候就會出現 B→A→B 的問題,形成死循環。
解決的方法:升級 JDK 版本,在 JDK 8 以後擴容不會再進行倒序,所以死循環的問題獲得了極大的改善,但這不是終極的方案,由於 HashMap 原本就不是用在多線程版本下的,若是是多線程可以使用 ConcurrentHashMap 替代 HashMap。
A:Hashtable 和 HashMap 都是非線程安全的
B:ConcurrentHashMap 容許 null 做爲 key
C:HashMap 容許 null 做爲 key
D:Hashtable 容許 null 做爲 key
答:C
題目解析:Hashtable 是線程安全的,ConcurrentHashMap 和 Hashtable 是不容許 null 做爲鍵和值的。
答:使用 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
A:LinedHashSet
B:HashSet
C:TreeSet
D:AbstractSet
答:C
Hashtable hashtable = new Hashtable(); hashtable.put("table", null); System.out.println(hashtable.get("table"));
答:程序執行報錯:java.lang.NullPointerException。Hashtable 不容許 null 鍵和值。
答:HashMap 有兩個重要的參數:容量(Capacity)和負載因子(LoadFactor)。
答:HashMap 和 Hashtable 區別以下:
答:當輸入兩個不一樣值,根據同一散列函數計算出相同的散列值的現象,咱們就把它叫作碰撞(哈希碰撞)。
答:哈希衝突的經常使用解決方案有如下 4 種。
答:HashMap 使用鏈表和紅黑樹來解決哈希衝突,詳見本文 put() 方法的執行過程。
答:這樣作的目的是爲了讓散列更加均勻,從而減小哈希碰撞,以提供代碼的執行效率。
答:若是有哈希衝突,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); // 看看是否還有下一個節點,若是有,繼續下一輪比對,不然跳出循環
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。
答:由於 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()。
答:HashMap 在 JDK 7 中會致使死循環的問題。由於在 JDK 7 中,多線程進行 HashMap 擴容時會致使鏈表的循環引用,這個時候使用 get() 獲取元素時就會致使死循環,形成 CPU 100% 的狀況。
答:HashMap 在 JDK 7 和 JDK 8 的主要區別以下。
經過本文能夠了解到:
HashMap 在 JDK 7 可能在擴容時會致使鏈表的循環引用而形成 CPU 100%,HashMap 在 JDK 8 時數據結構變動爲:數組 + 鏈表 + 紅黑樹的存儲方式,在沒有衝突的狀況下直接存放數組,有衝突,當鏈表長度小於 8 時,存放在單鏈表結構中,當鏈表長度大於 8 時,樹化並存放至紅黑樹的數據結構中。
_
歡迎關注個人公衆號,回覆關鍵字「Java」 ,將會有大禮相送!!! 祝各位面試成功!!!