Java集合必會14問(精選面試題整理)

參考:https://www.jianshu.com/p/939b8a672070html

1)說說常見的集合有哪些吧?

答:Map接口和Collection接口是全部集合框架的父接口:java

  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的實現類主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  3. Set接口的實現類主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的實現類主要有:ArrayList、LinkedList、Stack以及Vector等

2)HashMap與HashTable的區別?

答:node

  1. HashMap沒有考慮同步,是線程不安全的;Hashtable使用了synchronized關鍵字,是線程安全的;面試

  2. HashMap容許K/V都爲null;後者K/V都不容許爲null;算法

  3. HashMap繼承自AbstractMap類;而Hashtable繼承自Dictionary類;編程

3)HashMap的put方法的具體流程?

圖引用自:blog.csdn.net/u011240877/…設計模式

答:下面先來分析一下源碼數組

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
    // 1.若是table爲空或者長度爲0,即沒有元素,那麼使用resize()方法擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2.計算插入存儲的數組索引i,此處計算方法同 1.7 中的indexFor()方法
    // 若是數組爲空,即不存在Hash衝突,則直接插入數組
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 3.插入時,若是發生Hash衝突,則依次往下判斷
    else {
        HashMap.Node<K,V> e; K k;
        // a.判斷table[i]的元素的key是否與須要插入的key同樣,若相同則直接用新的value覆蓋掉舊的value
        // 判斷原則equals() - 因此須要當key的對象重寫該方法
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // b.繼續判斷:須要插入的數據結構是紅黑樹仍是鏈表
        // 若是是紅黑樹,則直接在樹中插入 or 更新鍵值對
        else if (p instanceof HashMap.TreeNode)
            e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 若是是鏈表,則在鏈表中插入 or 更新鍵值對
        else {
            // i .遍歷table[i],判斷key是否已存在:採用equals對比當前遍歷結點的key與須要插入數據的key
            //    若是存在相同的,則直接覆蓋
            // ii.遍歷完畢後任務發現上述狀況,則直接在鏈表尾部插入數據
            //    插入完成後判斷鏈表長度是否 > 8:如果,則把鏈表轉換成紅黑樹
            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;
            }
        }
        // 對於i 狀況的後續操做:發現key已存在,直接用新value覆蓋舊value&返回舊value
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 插入成功後,判斷實際存在的鍵值對數量size > 最大容量
    // 若是大於則進行擴容
    if (++size > threshold)
        resize();
    // 插入成功時會調用的方法(默認實現爲空)
    afterNodeInsertion(evict);
    return null;
}
複製代碼

圖片簡單總結爲:緩存

4)HashMap的擴容操做是怎麼實現的?

答:經過分析源碼咱們知道了HashMap經過resize()方法進行擴容或者初始化的操做,下面是對源碼進行的一些簡單分析:安全

/**
 * 該函數有2中使用狀況:1.初始化哈希表;2.當前數組容量太小,須要擴容
 */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;// 擴容前的數組(當前數組)
    int oldCap = (oldTab == null) ? 0 : oldTab.length;// 擴容前的數組容量(數組長度)
    int oldThr = threshold;// 擴容前數組的閾值
    int newCap, newThr = 0;

    if (oldCap > 0) {
        // 針對狀況2:若擴容前的數組容量超過最大值,則再也不擴容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 針對狀況2:若沒有超過最大值,就擴容爲原來的2倍(左移1位)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }

    // 針對狀況1:初始化哈希表(採用指定或者使用默認值的方式)
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 計算新的resize上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 把每個bucket都移動到新的bucket中去
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
複製代碼

5)HashMap是怎麼解決哈希衝突的?

參考資料:juejin.im/post/5ab99a…

答:在解決這個問題以前,咱們首先須要知道什麼是哈希衝突,而在瞭解哈希衝突以前咱們還要知道什麼是哈希才行;

什麼是哈希?

Hash,通常翻譯爲「散列」,也有直接音譯爲「哈希」的,這就是把任意長度的輸入經過散列算法,變換成固定長度的輸出,該輸出就是散列值(哈希值);這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來惟一的肯定輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。

全部散列函數都有以下一個基本特性:根據同一散列函數計算出的散列值若是不一樣,那麼輸入值確定也不一樣。可是,根據同一散列函數計算出的散列值若是相同,輸入值不必定相同。

什麼是哈希衝突?

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

HashMap的數據結構

在Java中,保存數據有兩種比較簡單的數據結構:數組和鏈表。數組的特色是:尋址容易,插入和刪除困難;鏈表的特色是:尋址困難,但插入和刪除容易;因此咱們將數組和鏈表結合在一塊兒,發揮二者各自的優點,使用一種叫作鏈地址法的方式能夠解決哈希衝突:

這樣咱們就能夠將擁有相同哈希值的對象組織成一個鏈表放在hash值所對應的bucket下,但相比於hashCode返回的int類型,咱們HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4(即2的四次方16)要遠小於int類型的範圍,因此咱們若是隻是單純的用hashCode取餘來獲取對應的bucket這將會大大增長哈希碰撞的機率,而且最壞狀況下還會將HashMap變成一個單鏈表,因此咱們還須要對hashCode做必定的優化

hash()函數

上面提到的問題,主要是由於若是使用hashCode取餘,那麼至關於參與運算的只有hashCode的低位,高位是沒有起到任何做用的,因此咱們的思路就是讓hashCode取值出的高位也參與運算,進一步下降hash碰撞的機率,使得數據分佈更平均,咱們把這樣的操做稱爲擾動,在JDK 1.8中的hash()函數以下:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 與本身右移16位進行異或運算(高低位異或)
}
複製代碼

這比在JDK 1.7中,更爲簡潔,相比在1.7中的4次位運算,5次異或運算(9次擾動),在1.8中,只進行了1次位運算和1次異或運算(2次擾動);

JDK1.8新增紅黑樹

經過上面的鏈地址法(使用散列表)擾動函數咱們成功讓咱們的數據分佈更平均,哈希碰撞減小,可是當咱們的HashMap中存在大量數據時,加入咱們某個bucket下對應的鏈表有n個元素,那麼遍歷時間複雜度就爲O(n),爲了針對這個問題,JDK1.8在HashMap中新增了紅黑樹的數據結構,進一步使得遍歷複雜度下降至O(logn);

總結

簡單總結一下HashMap是使用了哪些方法來有效解決哈希衝突的:

1. 使用鏈地址法(使用散列表)來連接擁有相同hash值的數據;
2. 使用2次擾動函數(hash函數)來下降哈希衝突的機率,使得數據分佈更平均;
3. 引入紅黑樹進一步下降遍歷的時間複雜度,使得遍歷更快;

6)HashMap爲何不直接使用hashCode()處理後的哈希值直接做爲table的下標?

答:hashCode()方法返回的是int整數類型,其範圍爲-(2 ^ 31)~(2 ^ 31 - 1),約有40億個映射空間,而HashMap的容量範圍是在16(初始化默認值)~2 ^ 30,HashMap一般狀況下是取不到最大值的,而且設備上也難以提供這麼多的存儲空間,從而致使經過hashCode()計算出的哈希值可能不在數組大小範圍內,進而沒法匹配存儲位置;

面試官:那怎麼解決呢?

答:

  1. HashMap本身實現了本身的hash()方法,經過兩次擾動使得它本身的哈希值高低位自行進行異或運算,下降哈希碰撞機率也使得數據分佈更平均;

  2. 在保證數組長度爲2的冪次方的時候,使用hash()運算以後的值與運算(&)(數組長度 - 1)來獲取數組下標的方式進行存儲,這樣一來是比取餘操做更加有效率,二來也是由於只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,三來解決了「哈希值與數組大小範圍不匹配」的問題;

面試官:爲何數組長度要保證爲2的冪次方呢?

答:

  1. 只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,即實現了key的定位,2的冪次方也能夠減小衝突次數,提升HashMap的查詢效率;

  2. 若是 length 爲 2 的次冪 則 length-1 轉化爲二進制一定是 11111……的形式,在於 h 的二進制與操做效率會很是的快,並且空間不浪費;若是 length 不是 2 的次冪,好比 length 爲 15,則 length - 1 爲 14,對應的二進制爲 1110,在於 h 與操做,最後一位都爲 0 ,而 0001,0011,0101,1001,1011,0111,1101 這幾個位置永遠都不能存放元素了,空間浪費至關大,更糟的是這種狀況中,數組可使用的位置比數組長度小了不少,這意味着進一步增長了碰撞的概率,減慢了查詢的效率!這樣就會形成空間的浪費。

面試官:那爲何是兩次擾動呢?

答:這樣就是加大哈希值低位的隨機性,使得分佈更均勻,從而提升對應數組存儲下標位置的隨機性&均勻性,最終減小Hash衝突,兩次就夠了,已經達到了高位低位同時參與運算的目的;

7)HashMap在JDK1.7和JDK1.8中有哪些不一樣?

答:

不一樣 JDK 1.7 JDK 1.8
存儲結構 數組 + 鏈表 數組 + 鏈表 + 紅黑樹
初始化方式 單獨函數:inflateTable() 直接集成到了擴容函數resize()
hash值計算方式 擾動處理 = 9次擾動 = 4次位運算 + 5次異或運算 擾動處理 = 2次擾動 = 1次位運算 + 1次異或運算
存放數據的規則 無衝突時,存放數組;衝突時,存放鏈表 無衝突時,存放數組;衝突 & 鏈表長度 < 8:存放單鏈表;衝突 & 鏈表長度 > 8:樹化並存放紅黑樹
插入數據方式 頭插法(先講原位置的數據移到後1位,再插入數據到該位置) 尾插法(直接插入到鏈表尾部/紅黑樹)
擴容後存儲位置的計算方式 所有按照原來方法進行計算(即hashCode ->> 擾動函數 ->> (h&length-1)) 按照擴容後的規律計算(即擴容後的位置=原位置 or 原位置 + 舊容量)

8)爲何HashMap中String、Integer這樣的包裝類適合做爲K?

答:String、Integer等包裝類的特性可以保證Hash值的不可更改性和計算準確性,可以有效的減小Hash碰撞的概率

  1. 都是final類型,即不可變性,保證key的不可更改性,不會存在獲取hash值不一樣的狀況
  2. 內部已重寫了equals()hashCode()等方法,遵照了HashMap內部的規範(不清楚能夠去上面看看putValue的過程),不容易出現Hash值計算錯誤的狀況;

面試官:若是我想要讓本身的Object做爲K應該怎麼辦呢?

答:重寫hashCode()equals()方法

  1. 重寫hashCode()是由於須要計算存儲數據的存儲位置,須要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提升性能,這樣雖然能更快但可能會致使更多的Hash碰撞;

  2. 重寫equals()方法,須要遵照自反性、對稱性、傳遞性、一致性以及對於任何非null的引用值x,x.equals(null)必須返回false的這幾個特性,目的是爲了保證key在哈希表中的惟一性

9)ConcurrentHashMap和Hashtable的區別?

答:ConcurrentHashMap 結合了 HashMap 和 HashTable 兩者的優點。HashMap 沒有考慮同步,HashTable 考慮了同步的問題。可是 HashTable 在每次同步執行時都要鎖住整個結構。 ConcurrentHashMap 鎖的方式是稍微細粒度的。

面試官:ConcurrentHashMap的具體實現知道嗎?

參考資料:www.importnew.com/23610.html

答:在JDK1.7中,ConcurrentHashMap採用Segment + HashEntry的方式進行實現,結構以下:

  1. 該類包含兩個靜態內部類 HashEntry 和 Segment ;前者用來封裝映射表的鍵值對,後者用來充當鎖的角色;

  2. Segment 是一種可重入的鎖 ReentrantLock,每一個 Segment 守護一個HashEntry 數組裏得元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment 鎖。

JDK1.8中,放棄了Segment臃腫的設計,取而代之的是採用Node + CAS + Synchronized來保證併發安全進行實現,結構以下:

插入元素過程(建議去看看源碼):

  1. 若是相應位置的Node尚未初始化,則調用CAS插入相應的數據;
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;                   // no lock when adding to empty bin
}
複製代碼
  1. 若是相應位置的Node不爲空,且當前該節點不處於移動狀態,則對該節點加synchronized鎖,若是該節點的hash不小於0,則遍歷鏈表更新節點或插入新節點;
if (fh >= 0) {
    binCount = 1;
    for (Node<K,V> e = f;; ++binCount) {
        K ek;
        if (e.hash == hash &&
            ((ek = e.key) == key ||
             (ek != null && key.equals(ek)))) {
            oldVal = e.val;
            if (!onlyIfAbsent)
                e.val = value;
            break;
        }
        Node<K,V> pred = e;
        if ((e = e.next) == null) {
            pred.next = new Node<K,V>(hash, key, value, null);
            break;
        }
    }
}
複製代碼
  1. 若是該節點是TreeBin類型的節點,說明是紅黑樹結構,則經過putTreeVal方法往紅黑樹中插入節點;若是binCount不爲0,說明put操做對數據產生了影響,若是當前鏈表的個數達到8個,則經過treeifyBin方法轉化爲紅黑樹,若是oldVal不爲空,說明是一次更新操做,沒有對元素個數產生影響,則直接返回舊值;

  2. 若是插入的是一個新節點,則執行addCount()方法嘗試更新元素個數baseCount;

10)Java集合的快速失敗機制 「fail-fast」?

答:

是java集合的一種錯誤檢測機制,當多個線程對集合進行結構上的改變的操做時,有可能會產生 fail-fast 機制。

例如:假設存在兩個線程(線程一、線程2),線程1經過Iterator在遍歷集合A中的元素,在某個時候線程2修改了集合A的結構(是結構上面的修改,而不是簡單的修改集合元素的內容),那麼這個時候程序就會拋出 ConcurrentModificationException 異常,從而產生fail-fast機制。

緣由:迭代器在遍歷時直接訪問集合中的內容,而且在遍歷過程當中使用一個 modCount 變量。集合在被遍歷期間若是內容發生變化,就會改變modCount的值。每當迭代器使用hashNext()/next()遍歷下一個元素以前,都會檢測modCount變量是否爲expectedmodCount值,是的話就返回遍歷;不然拋出異常,終止遍歷。

解決辦法:

1. 在遍歷過程當中,全部涉及到改變modCount值得地方所有加上synchronized。

2. 使用CopyOnWriteArrayList來替換ArrayList

11)ArrayList 和 Vector 的區別?

答:

這兩個類都實現了 List 接口(List 接口繼承了 Collection 接口),他們都是有序集合,即存儲在這兩個集合中的元素位置都是有順序的,至關於一種動態的數組,咱們之後能夠按位置索引來取出某個元素,而且其中的數據是容許重複的,這是與 HashSet 之類的集合的最大不一樣處,HashSet 之類的集合不能夠按索引號去檢索其中的元素,也不容許有重複的元素。

ArrayList 與 Vector 的區別主要包括兩個方面:

  1. 同步性:
    Vector 是線程安全的,也就是說它的方法之間是線程同步(加了synchronized 關鍵字)的,而 ArrayList 是線程不安全的,它的方法之間是線程不一樣步的。若是隻有一個線程會訪問到集合,那最好是使用 ArrayList,由於它不考慮線程安全的問題,因此效率會高一些;若是有多個線程會訪問到集合,那最好是使用 Vector,由於不須要咱們本身再去考慮和編寫線程安全的代碼。

  2. 數據增加:
    ArrayList 與 Vector 都有一個初始的容量大小,當存儲進它們裏面的元素的我的超過了容量時,就須要增長 ArrayList 和 Vector 的存儲空間,每次要增長存儲空間時,不是隻增長一個存儲單元,而是增長多個存儲單元,每次增長的存儲單元的個數在內存空間利用與程序效率之間要去的必定的平衡。Vector 在數據滿時(加載因子1)增加爲原來的兩倍(擴容增量:原容量的 2 倍),而 ArrayList 在數據量達到容量的一半時(加載因子 0.5)增加爲原容量的 (0.5 倍 + 1) 個空間。

12)ArrayList和LinkedList的區別?

答:

  1. LinkedList 實現了 List 和 Deque 接口,通常稱爲雙向鏈表;ArrayList 實現了 List 接口,動態數組;
  2. LinkedList 在插入和刪除數據時效率更高,ArrayList 在查找某個 index 的數據時效率更高;
  3. LinkedList 比 ArrayList 須要更多的內存;

面試官:Array 和 ArrayList 有什麼區別?何時該應 Array 而不是 ArrayList 呢?

答:它們的區別是:

  1. Array 能夠包含基本類型和對象類型,ArrayList 只能包含對象類型。
  2. Array 大小是固定的,ArrayList 的大小是動態變化的。
  3. ArrayList 提供了更多的方法和特性,好比:addAll(),removeAll(),iterator() 等等。

對於基本類型數據,集合使用自動裝箱來減小編碼工做量。可是,當處理固定大小的基本數據類型的時候,這種方式相對比較慢。

13)HashSet是如何保證數據不可重複的?

答:HashSet的底層其實就是HashMap,只不過咱們HashSet是實現了Set接口而且把數據做爲K值,而V值一直使用一個相同的虛值來保存,咱們能夠看到源碼:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;// 調用HashMap的put方法,PRESENT是一個至始至終都相同的虛值
}
複製代碼

因爲HashMap的K值自己就不容許重複,而且在HashMap中若是K/V相同時,會用新的V覆蓋掉舊的V,而後返回舊的V,那麼在HashSet中執行這一句話始終會返回一個false,致使插入失敗,這樣就保證了數據的不可重複性;

14)BlockingQueue是什麼?

答:

Java.util.concurrent.BlockingQueue是一個隊列,在進行檢索或移除一個元素的時候,它會等待隊列變爲非空;當在添加一個元素時,它會等待隊列中的可用空間。BlockingQueue接口是Java集合框架的一部分,主要用於實現生產者-消費者模式。咱們不須要擔憂等待生產者有可用的空間,或消費者有可用的對象,由於它都在BlockingQueue的實現類中被處理了。Java提供了集中BlockingQueue的實現,好比ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。



HashMap面試

參考:https://mp.weixin.qq.com/s/2cHStiQTe1R4FSPbnk9TOw

如今是晚上11點了,學校屠豬館的自習室由於太晚要關閉了,勤奮且疲憊的小魯班也從屠豬館出來了,正準備回宿舍洗洗睡,因爲自習室位置比較偏僻因此是接收不到手機網絡信號的,所以小魯班從兜裏掏出手機的時候,信息可真是炸了呀,小魯班心想,微信羣平時都沒什麼人聊天,今晚確定是發生了什麼大事,仔細一看,才發現原來是小魯班的室友達摩(光頭)拿到了阿里巴巴JAVA開發實習生的offer,此時小魯班真替他室友感到高興的同時,內心也不免會產生一絲絲的失落感,那是由於本身投了不少份簡歷,別說拿不拿獲得offer,就連給面試邀的公司也都寥寥無幾,小魯班這會可真是受到了一萬點真實暴擊,不太小魯班仍是很樂觀的,很快調整了心態,帶上耳機,慢慢的走回了宿舍,正打算準備向他那神室友達摩取取經。


片刻後~


小魯班:666,據說你拿到了阿里的offer,能透露一下面試內容和技巧嗎

達摩:嘿嘿嘿,沒問題鴨,叫聲爸爸我就告訴你

小魯班:baba(表面笑嘻嘻,內心MMP)

達摩:其實我也不是很記得了(請繼續裝),但我仍是記得那麼一些,若是你是面的JAVA,首先固然是


  • JAVA的基礎知識:數據結構(Map,List,Set等),設計模式,算法,線程相關,IO/NIO,序列化等等

  • 其次是高級特徵:反射機制,併發與鎖,JVM(GC策略,類加載機制,內存模型)等等


小魯班:問這麼多內容,那豈不是一我的都面試好久嗎?

達摩:不是的,面試官通常都會用連環炮的方式提問的。

小魯班:你說的連環炮是什麼意思鴨?

達摩:那我舉個例子


就好比問你HashMap是否是有序的?


你回答不是有序的。那面試官就會可能繼續問你,有沒有有序的Map實現類呢?


你若是這個時候說不知道的話,那這塊問題就到此結束了。若是你說有TreeMap和LinkedHashMap。


那麼面試官接下來就可能會問你,TreeMap和LinkedHashMap是如何保證它的順序的?


若是你回答不上來,那麼到此爲止。若是你說TreeMap是經過實現SortMap接口,可以把它保存的鍵值對根據key排序,基於紅黑樹,從而保證TreeMap中全部鍵值對處於有序狀 態。LinkedHashMap則是經過插入排序(就是你put的時候的順序是什麼,取出來的時候就是什麼樣子)和訪問排序(改變排序把訪問過的放到底部)讓鍵值有序。


那麼面試官還會繼續問你,你以爲它們兩個哪一個的有序實現比較好?


若是你依然能夠回答的話,那麼面試官會繼續問你,你以爲還有沒有比它更好或者更高效的實現方式。。無窮無盡深刻,直到你回答不出來或者面試官認爲問題到底了


小魯班捏了一把汗,我去。。。這是魔鬼吧,那咱們來試試唄(由於小魯班剛剛在自習室纔看了這章的知識,想趁機裝一波逼,畢竟剛剛叫了聲爸爸~~)


因而達摩and小魯班就開始了對決:


一、爲何用HashMap?


  • HashMap是一個散列桶(數組和鏈表),它存儲的內容是鍵值對(key-value)映射

  • HashMap採用了數組和鏈表的數據結構,能在查詢和修改方便繼承了數組的線性查找和鏈表的尋址修改

  • HashMap是非synchronized,因此HashMap很快

  • HashMap能夠接受null鍵和值,而Hashtable則不能(緣由就是equlas()方法須要對象,由於HashMap是後出的API通過處理才能夠)


二、HashMap的工做原理是什麼?


  • HashMap是基於hashing的原理,咱們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當咱們給put()方法傳遞鍵和值時,咱們先對鍵調用hashCode()方法,計算並返回的hashCode是用於找到Map數組的bucket位置來儲存Node 對象。這裏關鍵點在於指出,HashMap是在bucket中儲存鍵對象和值對象,做爲Map.Node 。


 

  • 如下是HashMap初始化 ,簡單模擬數據結構


Node[] table=new Node[16]  散列桶初始化,table
class Node {
 hash;//hash值
      key;//鍵
 value;//值
 node next;//用於指向鏈表的下一層(產生衝突,用拉鍊法)

}
複製代碼


  • 如下是具體的put過程(JDK1.8版)


一、對Key求Hash值,而後再計算下標

二、若是沒有碰撞,直接放入桶中(碰撞的意思是計算獲得的Hash值相同,須要放到同一個bucket中)

三、若是碰撞了,以鏈表的方式連接到後面

四、若是鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表

五、若是節點已經存在就替換舊值

六、若是桶滿了(容量16*加載因子0.75),就須要 resize(擴容2倍後重排)


  • 如下是具體get過程(考慮特殊狀況若是兩個鍵的hashcode相同,你如何獲取值對象?)


當咱們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,找到bucket位置以後,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。


三、有什麼方法能夠減小碰撞?


  • 擾動函數能夠減小碰撞,原理是若是兩個不相等的對象返回不一樣的hashcode的話,那麼碰撞的概率就會小些,這就意味着存鏈表結構減少,這樣取值的話就不會頻繁調用equal方法,這樣就能提升HashMap的性能。(擾動即Hash方法內部的算法實現,目的是讓不一樣對象返回不一樣hashcode。)

  • 使用不可變的、聲明做final的對象,而且採用合適的equals()和hashCode()方法的話,將會減小碰撞的發生。不可變性使得可以緩存不一樣鍵的hashcode,這將提升整個獲取對象的速度,使用String,Interger這樣的wrapper類做爲鍵是很是好的選擇。爲何String, Interger這樣的wrapper類適合做爲鍵?由於String是final的,並且已經重寫了equals()和hashCode()方法了。不可變性是必要的,由於爲了要計算hashCode(),就要防止鍵值改變,若是鍵值在放入時和獲取時返回不一樣的hashcode的話,那麼就不能從HashMap中找到你想要的對象。


四、HashMap中hash函數怎麼是是實現的?


咱們能夠看到在hashmap中要找到某個元素,須要根據key的hash值來求得對應數組中的位置。如何計算這個位置就是hash算法。前面說過hashmap的數據結構是數組和鏈表的結合,因此咱們固然但願這個hashmap裏面的元素位置儘可能的分佈均勻些,儘可能使得每一個位置上的元素數量只有一個,那麼當咱們用hash算法求得這個位置的時候,立刻就能夠知道對應位置的元素就是咱們要的,而不用再去遍歷鏈表。 因此咱們首先想到的就是把hashcode對數組長度取模運算,這樣一來,元素的分佈相對來講是比較均勻的。可是,「模」運算的消耗仍是比較大的,能不能找一種更快速,消耗更小的方式,咱們來看看JDK1.8的源碼是怎麼作的(被樓主修飾了一下)


static final int hash(Object key) {
    if (key == null){
        return 0;
    }
     int h;
     h=key.hashCode();返回散列值也就是hashcode
      // ^ :按位異或
      // >>>:無符號右移,忽略符號位,空位都以0補齊
      //其中n是數組的長度,即Map的數組部分初始化長度
     return  (n-1)&(h ^ (h >>> 16));
}
複製代碼

簡單來講就是

一、高16bt不變,低16bit和高16bit作了一個異或(獲得的HASHCODE轉化爲32位的二進制,前16位和後16位低16bit和高16bit作了一個異或)

二、(n·1)&hash=->獲得下標

五、拉鍊法致使的鏈表過深問題爲何不用二叉查找樹代替,而選擇紅黑樹?爲何不一直使用紅黑樹?

之因此選擇紅黑樹是爲了解決二叉查找樹的缺陷,二叉查找樹在特殊狀況下會變成一條線性結構(這就跟原來使用鏈表結構同樣了,形成很深的問題),遍歷查找會很是慢。而紅黑樹在插入新數據後可能須要經過左旋,右旋、變色這些操做來保持平衡,引入紅黑樹就是爲了查找數據快,解決鏈表查詢深度的問題,咱們知道紅黑樹屬於平衡二叉樹,可是爲了保持「平衡」是須要付出代價的,可是該代價所損耗的資源要比遍歷線性鏈表要少,因此當長度大於8的時候,會使用紅黑樹,若是鏈表長度很短的話,根本不須要引入紅黑樹,引入反而會慢。


六、說說你對紅黑樹的看法?


  

一、每一個節點非紅即黑

二、根節點老是黑色的

三、若是節點是紅色的,則它的子節點必須是黑色的(反之不必定)

四、每一個葉子節點都是黑色的空節點(NIL節點)

五、從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)


七、解決hash 碰撞還有那些辦法?


開放定址法。


當衝突發生時,使用某種探查技術在散列表中造成一個探查(測)序列。沿此序列逐個單元地查找,直到找到給定的地址。


按照造成探查序列的方法不一樣,可將開放定址法區分爲線性探查法、二次探查法、雙重散列法等。


下面給一個線性探查法的例子  


問題:已知一組關鍵字爲(26,36,41,38,44,15,68,12,06,51),用除餘法構造散列函數,用線性探查法解決衝突構造這組關鍵字的散列表。


解答:爲了減小衝突,一般令裝填因子α由除餘法因子是13的散列函數計算出的上述關鍵字序列的散列地址爲(0,10,2,12,5,2,3,12,6,12)。


前5個關鍵字插入時,其相應的地址均爲開放地址,故將它們直接插入T[0],T[10),T[2],T[12]和T[5]中。


當插入第6個關鍵字15時,其散列地址2(即h(15)=15%13=2)已被關鍵字41(15和41互爲同義詞)佔用。故探查h1=(2+1)%13=3,此地址開放,因此將15放入T[3]中。


當插入第7個關鍵字68時,其散列地址3已被非同義詞15先佔用,故將其插入到T[4]中。


當插入第8個關鍵字12時,散列地址12已被同義詞38佔用,故探查hl=(12+1)%13=0,而T[0]亦被26佔用,再探查h2=(12+2)%13=1,此地址開放,可將12插入其中。

相似地,第9個關鍵字06直接插入T[6]中;而最後一個關鍵字51插人時,因探查的地址12,0,1,…,6均非空,故51插入T[7]中。


八、若是HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?


默認的負載因子大小爲0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)同樣,將會建立原來HashMap大小的兩倍的bucket數組,來從新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫做rehashing,由於它調用hash方法找到新的bucket位置。這個值只可能在兩個地方,一個是原下標的位置,另外一種是在下標爲<原下標+原容量>的位置  


九、從新調整HashMap大小存在什麼問題嗎?


  • 當從新調整HashMap大小的時候,確實存在條件競爭,由於若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。(多線程的環境下不使用HashMap)

  • 爲何多線程會致使死循環,它是怎麼發生的?


HashMap的容量是有限的。當通過屢次元素插入,使得HashMap達到必定飽和度時,Key映射位置發生衝突的概率會逐漸提升。這時候,HashMap須要擴展它的長度,也就是進行Resize。1.擴容:建立一個新的Entry空數組,長度是原數組的2倍。2.ReHash:遍歷原Entry數組,把全部的Entry從新Hash到新數組。


(這個過程比較燒腦,暫不做流程圖演示,有興趣去看看個人另外一篇博文"HashMap擴容全過程")

達摩:哎呦,小老弟不錯嘛~~意料以外呀

小魯班:嘿嘿,優秀吧,中場休息一波,我先喝口水

達摩:不只僅是這些哦,面試官還會問你相關的集合類對比,好比:


十、HashTable


  • 數組 + 鏈表方式存儲

  • 默認容量:11(質數 爲宜)

  • put:

    • 索引計算 : (key.hashCode() & 0x7FFFFFFF)% table.length

    • 若在鏈表中找到了,則替換舊值,若未找到則繼續

    • 當總元素個數超過容量*加載因子時,擴容爲原來 2 倍並從新散列。

    • 將新元素加到鏈表頭部

  • 對修改 Hashtable 內部共享數據的方法添加了 synchronized,保證線程安全。


十一、HashMap ,HashTable 區別


  • 默認容量不一樣。擴容不一樣

  • 線程安全性,HashTable 安全

  • 效率不一樣 HashTable 要慢由於加鎖


十二、ConcurrentHashMap 原理


一、最大特色是引入了 CAS(藉助 Unsafe 來實現【native code】)


  • CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。

  • Unsafe 藉助 CPU 指令 cmpxchg 來實現

  • 使用實例:


一、對 sizeCtl 的控制都是用 CAS 來實現的

一、sizeCtl :默認爲0,用來控制 table 的初始化和擴容操做。

  • -1 表明table正在初始化

  • N 表示有 -N-1 個線程正在進行擴容操做

  • 若是table未初始化,表示table須要初始化的大小。

  • 若是table初始化完成,表示table的容量,默認是table大小的0.75倍,竟然用這個公式算0.75(n - (n >>> 2))。


四、CAS 會出現的問題:ABA


  • 對變量增長一個版本號,每次修改,版本號加 1,比較的時候比較版本號。


1三、咱們可使用CocurrentHashMap來代替Hashtable嗎?


  • 咱們知道Hashtable是synchronized的,可是ConcurrentHashMap同步性能更好,由於它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap固然能夠代替HashTable,可是HashTable提供更強的線程安全性。它們均可以用於多線程的環境,可是當Hashtable的大小增長到必定的時候,性能會急劇降低,由於迭代時須要被鎖定很長的時間。由於ConcurrentHashMap引入了分割(segmentation),不論它變得多麼大,僅僅須要鎖定map的某個部分,而其它的線程不須要等到迭代完成才能訪問map。簡而言之,在迭代的過程當中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。

此時躺着牀上的張飛哄了一聲:睡覺了睡覺了~


見此不太妙:小魯班立馬回到牀上(泉水),把被子蓋過頭,內心有一絲絲愉悅感,不對。好像還沒洗澡。。。


by the way


CocurrentHashMap在JAVA8中存在一個bug,會進入死循環,緣由是遞歸建立ConcurrentHashMap 對象,可是在1.9已經修復了,場景重現以下


public class ConcurrentHashMapDemo{

    private Map<Integer,Integer> cache =new ConcurrentHashMap<>(15);

    public static void main(String[]args){
        ConcurrentHashMapDemo ch =    new ConcurrentHashMapDemo();
        System.out.println(ch.fibonaacci(80));        
    }

    public int fibonaacci(Integer i){        
        if(i==0||i ==1) {                
            return i;        
    }

    return cache.computeIfAbsent(i,(key) -> {
        System.out.println("fibonaacci : "+key);
        return fibonaacci(key -1)+fibonaacci(key - 2);        
    });       

    }
}複製代碼


讓我再擼一次HashMap

參考:https://mp.weixin.qq.com/s/7HlgNQmqnBZlFx5WQhzP0g

(1)HashMap的實現原理?
此題能夠組成以下連環炮來問

  • 你看過HashMap源碼嘛,知道原理嘛?

  • 爲何用數組+鏈表?

  • hash衝突你還知道哪些解決辦法?

  • 我用LinkedList代替數組結構能夠麼?

  • 既然是能夠的,爲何HashMap不用LinkedList,而選用數組?

你看過HashMap源碼嘛,知道原理嘛?


針對這個問題,嗯,固然是必須看過HashMap源碼。至於原理,下面那張圖很清楚了:

HashMap採用Entry數組來存儲key-value對,每個鍵值對組成了一個Entry實體,Entry類其實是一個單向的鏈表結構,它具備Next指針,能夠鏈接下一個Entry實體。
只是在JDK1.8中,鏈表長度大於8的時候,鏈表會轉成紅黑樹!

爲何用數組+鏈表?


數組是用來肯定桶的位置,利用元素的key的hash值對數組長度取模獲得.
鏈表是用來解決hash衝突問題,當出現hash值同樣的情形,就在數組上的對應位置造成一條鏈表。

ps:這裏的hash值並非指hashcode,而是將hashcode高低十六位異或過的。至於爲何要這麼作,繼續往下看。

hash衝突你還知道哪些解決辦法?


比較出名的有四種(1)開放定址法(2)鏈地址法(3)再哈希法(4)公共溢出區域法

ps:你們有興趣拓展的,本身去搜一下就懂了,這個就不拓展了!

我用LinkedList代替數組結構能夠麼?


這裏我稍微說明一下,此題的意思是,源碼中是這樣的

Entry[] table = new Entry[capacity];
複製代碼

ps:Entry就是一個鏈表節點。
那我用下面這樣表示

List<Entry> table = new LinkedList<Entry>();  
複製代碼

是否可行?
答案很明顯,必須是能夠的。

既然是能夠的,爲何HashMap不用LinkedList,而選用數組?


由於用數組效率最高!
在HashMap中,定位桶的位置是利用元素的key的哈希值對數組長度取模獲得。此時,咱們已獲得桶的位置。顯然數組的查找效率比LinkedList大。

那ArrayList,底層也是數組,查找也快啊,爲啥不用ArrayList?

(煙哥寫到這裏的時候,不由以爲本身真有想法,本身把本身問死了,還好我靈機一動想出了答案)
由於採用基本數組結構,擴容機制能夠本身定義,HashMap中數組擴容恰好是2的次冪,在作取模運算的效率高。
而ArrayList的擴容機制是1.5倍擴容,那ArrayList爲何是1.5倍擴容這就不在本文說明了。

(2)HashMap在什麼條件下擴容?
此題能夠組成以下連環炮來問

  • HashMap在什麼條件下擴容?

  • 爲何擴容是2的n次冪?

  • 爲何爲何要先高16位異或低16位再取模運算?

HashMap在什麼條件下擴容?

若是bucket滿了(超過load factor*current capacity),就要resize。
load factor爲0.75,爲了最大程度避免哈希衝突
current capacity爲當前數組大小。

爲何擴容是2的次冪?

HashMap爲了存取高效,要儘可能較少碰撞,就是要儘可能把數據分配均勻,每一個鏈表長度大體相同,這個實現就在把數據存到哪一個鏈表中的算法;這個算法實際就是取模,hash%length。
可是,你們都知道這種運算不如位移運算快。
所以,源碼中作了優化hash&(length-1)。
也就是說hash%length==hash&(length-1)
那爲何是2的n次方呢?
由於2的n次方實際就是1後面n個0,2的n次方-1,實際就是n個1。
例如長度爲8時候,3&(8-1)=3 2&(8-1)=2 ,不一樣位置上,不碰撞。
而長度爲5的時候,3&(5-1)=0 2&(5-1)=0,都在0上,出現碰撞了。
因此,保證容積是2的n次方,是爲了保證在作(length-1)的時候,每一位都能&1 ,也就是和1111……1111111進行與運算。

爲何爲何要先高16位異或低16位再取模運算?

我先曬一下,jdk1.8裏的hash方法。1.7的比較複雜,咱就不看了。



hashmap這麼作,只是爲了下降hash衝突的概率。

打個比方,當咱們的length爲16的時候,哈希碼(字符串「abcabcabcabcabc」的key對應的哈希碼)對(16-1)與操做,對於多個key生成的hashCode,只要哈希碼的後4位爲0,不論不論高位怎麼變化,最終的結果均爲0。
以下圖所示

而加上高16位異或低16位的「擾動函數」後,結果以下



能夠看到: 擾動函數優化前:1954974080 % 16 = 1954974080 & (16 - 1) = 0 擾動函數優化後:1955003654 % 16 = 1955003654 & (16 - 1) = 6 很顯然,減小了碰撞的概率。

(3)講講hashmap的get/put的過程?
此題能夠組成以下連環炮來問

  • 知道hashmap中put元素的過程是什麼樣麼?

  • 知道hashmap中get元素的過程是什麼樣麼?

  • 你還知道哪些hash算法?

  • 說說String中hashcode的實現?(此題不少大廠問過)

知道hashmap中put元素的過程是什麼樣麼?

對key的hashCode()作hash運算,計算index;
若是沒碰撞直接放到bucket裏;
若是碰撞了,以鏈表的形式存在buckets後;
若是碰撞致使鏈表過長(大於等於TREEIFY_THRESHOLD),就把鏈表轉換成紅黑樹(JDK1.8中的改動);
若是節點已經存在就替換old value(保證key的惟一性)
若是bucket滿了(超過load factor*current capacity),就要resize。

知道hashmap中get元素的過程是什麼樣麼?

對key的hashCode()作hash運算,計算index;
若是在bucket裏的第一個節點裏直接命中,則直接返回;
若是有衝突,則經過key.equals(k)去查找對應的Entry;

  • 若爲樹,則在樹中經過key.equals(k)查找,O(logn);

  • 若爲鏈表,則在鏈表中經過key.equals(k)查找,O(n)。


你還知道哪些hash算法?

先說一下hash算法幹嗎的,Hash函數是指把一個大範圍映射到一個小範圍。把大範圍映射到一個小範圍的目的每每是爲了節省空間,使得數據容易保存。
比較出名的有MurmurHash、MD四、MD5等等


說說String中hashcode的實現?(此題頻率很高)

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
複製代碼

String類中的hashCode計算方法仍是比較簡單的,就是以31爲權,每一位爲字符的ASCII值進行運算,用天然溢出來等效取模。

哈希計算公式能夠計爲s[0]31^(n-1) + s[1]31^(n-2) + … + s[n-1]
那爲何以31爲質數呢?
主要是由於31是一個奇質數,因此31*i=32*i-i=(i<<5)-i,這種位移與減法結合的計算相比通常的運算快不少。

(4)爲何hashmap的在鏈表元素數量超過8時改成紅黑樹?
此題能夠組成以下連環炮來問

  • 知道jdk1.8中hashmap改了啥麼?

  • 爲何在解決hash衝突的時候,不直接用紅黑樹?而選擇先用鏈表,再轉紅黑樹?

  • 我不用紅黑樹,用二叉查找樹能夠麼?

  • 那爲何閥值是8呢?

  • 當鏈表轉爲紅黑樹後,何時退化爲鏈表?


知道jdk1.8中hashmap改了啥麼?


  • 數組+鏈表的結構改成數組+鏈表+紅黑樹

  • 優化了高位運算的hash算法:h^(h>>>16)

  • 擴容後,元素要麼是在原位置,要麼是在原位置再移動2次冪的位置,且鏈表順序不變。

最後一條是重點,由於最後一條的變更,hashmap在1.8中,不會在出現死循環問題。

爲何在解決hash衝突的時候,不直接用紅黑樹?而選擇先用鏈表,再轉紅黑樹?

由於紅黑樹須要進行左旋,右旋,變色這些操做來保持平衡,而單鏈表不須要。
當元素小於8個當時候,此時作查詢操做,鏈表結構已經能保證查詢性能。當元素大於8個的時候,此時須要紅黑樹來加快查詢速度,可是新增節點的效率變慢了。

所以,若是一開始就用紅黑樹結構,元素太少,新增效率又比較慢,無疑這是浪費性能的。

我不用紅黑樹,用二叉查找樹能夠麼?

能夠。可是二叉查找樹在特殊狀況下會變成一條線性結構(這就跟原來使用鏈表結構同樣了,形成很深的問題),遍歷查找會很是慢。

那爲何閥值是8呢?

不知道,等jdk做者來回答。
這道題,網上能找到的答案都是扯淡。
我隨便貼一個牛客網的答案,以下圖所示

看出bug沒?交點是6.64?交點分明是4,好麼。
log4=2,4/2=2。
jdk做者選擇8,必定通過了嚴格的運算,以爲在長度爲8的時候,與其保證鏈表結構的查找開銷,不如轉換爲紅黑樹,改成維持其平衡開銷。

當鏈表轉爲紅黑樹後,何時退化爲鏈表?

爲6的時候退轉爲鏈表。中間有個差值7能夠防止鏈表和樹之間頻繁的轉換。假設一下,若是設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,若是一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。

(5)HashMap的併發問題?
此題能夠組成以下連環炮來問

  • HashMap在併發編程環境下有什麼問題啊?

  • 在jdk1.8中還有這些問題麼?

  • 你通常怎麼解決這些問題的?

HashMap在併發編程環境下有什麼問題啊?

  • (1)多線程擴容,引發的死循環問題

  • (2)多線程put的時候可能致使元素丟失

  • (3)put非null元素後get出來的倒是null

在jdk1.8中還有這些問題麼?

在jdk1.8中,死循環問題已經解決。其餘兩個問題仍是存在。

你通常怎麼解決這些問題的?

好比ConcurrentHashmap,Hashtable等線程安全等集合類。

(6)你通常用什麼做爲HashMap的key?
此題能夠組成以下連環炮來問

  • 健能夠爲Null值麼?

  • 你通常用什麼做爲HashMap的key?

  • 我用可變類當HashMap的key有什麼問題?

  • 若是讓你實現一個自定義的class做爲HashMap的key該如何實現?

健能夠爲Null值麼?

必須能夠,key爲null的時候,hash算法最後的值以0來計算,也就是放在數組的第一個位置。

你通常用什麼做爲HashMap的key?

通常用Integer、String這種不可變類當HashMap當key,並且String最爲經常使用。
  • (1)由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。

  • (2)由於獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是很是重要的,這些類已經很規範的覆寫了hashCode()以及equals()方法。


我用可變類當HashMap的key有什麼問題?

hashcode可能發生改變,致使put進去的值,沒法get出,以下所示

HashMap<List<String>, Object> changeMap = new HashMap<>();
List<String> list = new ArrayList<>();
list.add("hello");
Object objectValue = new Object();
changeMap.put(list, objectValue);
System.out.println(changeMap.get(list));
list.add("hello world");//hashcode發生了改變
System.out.println(changeMap.get(list));
複製代碼

輸出值以下

java.lang.Object@74a14482
null
複製代碼

若是讓你實現一個自定義的class做爲HashMap的key該如何實現?

此題考察兩個知識點

  • 重寫hashcode和equals方法注意什麼?

  • 如何設計一個不變類

針對問題一,記住下面四個原則便可
(1)兩個對象相等,hashcode必定相等
(2)兩個對象不等,hashcode不必定不等
(3)hashcode相等,兩個對象不必定相等
(4)hashcode不等,兩個對象必定不等
針對問題二,記住如何寫一個不可變類
(1)類添加final修飾符,保證類不被繼承。
若是類能夠被繼承會破壞類的不可變性機制,只要繼承類覆蓋父類的方法而且繼承類能夠改變成員變量值,那麼一旦子類以父類的形式出現時,不能保證當前類是否可變。

(2)保證全部成員變量必須私有,而且加上final修飾
經過這種方式保證成員變量不可改變。但只作到這一步還不夠,由於若是是對象成員變量有可能再外部改變其值。因此第4點彌補這個不足。

(3)不提供改變成員變量的方法,包括setter
避免經過其餘接口改變成員變量的值,破壞不可變特性。

(4)經過構造器初始化全部成員,進行深拷貝(deep copy)
若是構造器傳入的對象直接賦值給成員變量,仍是能夠經過對傳入對象的修改進而致使改變內部變量的值。例如:

public final class ImmutableDemo {  
    private final int[] myArray;  
    public ImmutableDemo(int[] array) {  
        this.myArray = array; // wrong  
    }  
}
複製代碼

這種方式不能保證不可變性,myArray和array指向同一塊內存地址,用戶能夠在ImmutableDemo以外經過修改array對象的值來改變myArray內部的值。
爲了保證內部的值不被修改,能夠採用深度copy來建立一個新內存保存傳入的值。正確作法:

public final class MyImmutableDemo {  
    private final int[] myArray;  
    public MyImmutableDemo(int[] array) {  
        this.myArray = array.clone();   
    }   
}
複製代碼

(5)在getter方法中,不要直接返回對象自己,而是克隆對象,並返回對象的拷貝這種作法也是防止對象外泄,防止經過getter得到內部可變成員對象後對成員變量直接操做,致使成員變量發生改變。

相關文章
相關標籤/搜索