Java容器集合面試精選

1.1 說說集合容器經常使用的集合類?

Java集合框架主要包括兩種類型的容器,一種是集合(Collection),存儲一個元素集合 ,另外一種是圖(Map),存儲鍵/值對映射。java

Collection

  • Collection集合的子接口有Set、List、Queue三種子接口,經常使用的List接口和Set接口。
  • List接口主要實現類:ArrayListLinkedListVectorStack
  • Set接口的主要實現類:HashSetTreeSetLinkedHashSet
  • Map接口的主要實現類:HashMapTreeMapHashtableLinkedHashMapConcurrentHashMap以及Properties

1.2 List,Set,Map三者的區別?List、Map、Set 三個接口存取元素時,各有什麼特色?

  • List:一個有序(元素存入集合的順序和取出的順序一致)容器,元素能夠重複,能夠插入多個null元素,元素都有索引。
  • Set:一個無序(存入和取出順序有可能不一致)容器,不能夠存儲重複元素,只容許存入一個null元素,必須保證元素惟一性。
  • Map:是一個鍵值對集合,存儲鍵、值和之間的映射。Key無序,惟一;value 不要求有序,容許重複。

1.3 ArrayList 和 LinkedList 的區別是什麼?

  • 底層數據結構實現ArrayList 底層使用的是動態數組,而 LinkedList 底層使用的是雙向鏈表。雙向鏈表
  • 隨機訪問效率ArrayListLinkedList 在隨機訪問的時候效率要高,ArrayList 實現了 RandomAccess 接口,而LinkedList 是線性的數據存儲方式,因此須要移動指針從前日後依次查找。
  • 插入和刪除效率ArrayList 底層採用數組存儲,因此插入和刪除元素 的時間複雜度受元素位置的影響。 好比:執行add(E e)方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種狀況時間複雜度就是O(1)。可是若是要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間複雜度就爲 O(n-i)。由於在進 行上述操做的時候集合中第 i 和第 i 個元素以後的(n-i)個元素都要執行向後位/向前移一位的 操做。 LinkedList 底層採用鏈表存儲,因此對於add(E e)方法的插入,刪除元素時間複雜 度不受元素位置的影響,近似 O(1),若是是要在指定位置i插入和刪除元素的話 add(int index, E element)時間複雜度近似爲o(n)由於須要先移動到指定位置 再插入。
  • 內存空間佔用LinkedListArrayList 更佔內存,由於 LinkedList 的節點除了存儲數據,還存儲了兩個引用,一個指向前一個元素,一個指向後一個元素。
  • 線程安全:ArrayList 和 LinkedList 都是不一樣步的,也就是不保證線程安全。

補充內容:RandomAccess接口node

public interface RandomAccess {
}

查看源碼咱們發現實際上 RandomAccess 接口中什麼都沒有定義。因此,在我看來 RandomAccess 接口不過是一個標識罷了。標識什麼? 標識實現這個接口的類具備隨機訪問功能。面試

在 binarySearch( )方法中,它要判斷傳入的list 是否 RamdomAccess 的實例,若是是,調用 indexedBinarySearch() 方法,若是不是,那麼調用 iteratorBinarySearch() 方法算法

public static <T>
    int binarySearch(List<? extends Comparable<? super Tjk list, T key)
{
if (list instanceof RandomAccess || list.size() <BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key); else
}
return Collections.iteratorBinarySearch(list, key);

ArrayList 實現了 RandomAccess 接口, 而 LinkedList 沒有實現。爲何呢?我以爲仍是 和底層數據結構有關! ArrayList 底層是數組,而 LinkedList 底層是鏈表。數組自然支持隨機 訪問,時間複雜度爲 O(1),因此稱爲快速隨機訪問。鏈表須要遍歷到特定位置才能訪問特定位置的 元素,時間複雜度爲 O(n),因此不支持快速隨機訪問。, ArrayList 實現了 RandomAccess 接 口,就代表了他具備快速隨機訪問功能。 RandomAccess 接口只是標識,並非說 ArrayList 實 現 RandomAccess 接口才具備快速隨機訪問功能的!編程

下面再總結一下 list 的遍歷方式選擇:數組

  • 實現了 RandomAccess 接口的list,優先選擇普通 for 循環 ,其次 foreach。
  • 未實現 RandomAccess 接口的list,優先選擇iterator遍歷(foreach遍歷底層也是經過 iterator實現的,),大size的數據,千萬不要使用普通for循環。

1.4 ArrayList 和 Vector 的區別是什麼?

  • 線程安全Vector 使用了 Synchronized 來實現線程同步,是線程安全的,而 ArrayList 是非線程安全的。
  • 性能ArrayList 在性能方面要優於 Vector
  • 擴容ArrayListVector 都會根據實際的須要動態的調整容量,只不過在 Vector 擴容每次會增長 1 倍,而 ArrayList 只會增長 50%。

綜上所述:安全

  • ArrayList、Vector 底層的實現是數組, 支持隨機訪問,因此適合作查詢的操做。可是Vector 中的方法因爲加了 synchronized 修飾,所以 Vector 是線程安全容器,但性能上較ArrayList差
  • LinkedList 使用雙向鏈表實現存儲,按序號索引數據須要進行前向或後向遍歷,但插入數據時只須要記錄當前項的先後項便可,因此 LinkedList 插入速度較快

補充說明:爲何 ArrayList 的 elementData 加上 transient 修飾數據結構

ArrayList 中的數組定義以下:多線程

private transient Object[] elementData;

再看一下 ArrayList 的定義:併發

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

能夠看到 ArrayList 實現了 Serializable 接口,這意味着 ArrayList 支持序列化。transient 的做用是說不但願 elementData 數組被序列化,重寫了 writeObject 實現:

private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
    *// Write out element count, and any hidden stuff*
        int expectedModCount = modCount;
    s.defaultWriteObject();
    *// Write out array length*
        s.writeInt(elementData.length);
    *// Write out all elements in the proper order.*
        for (int i=0; i<size; i++)
            s.writeObject(elementData[i]);
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
}

每次序列化時,先調用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,而後遍歷 elementData,只序列化已存入的元素,這樣既加快了序列化的速度,又減少了序列化以後的文件大小。


1.5 HashSet如何檢查重複?HashSet是如何保證數據不可重複的?

向HashSet 中add ()元素時,判斷元素是否存在的依據,不只要比較hash值,同時還要結合equles 方法比較。HashSet 中的add ()方法會使用HashMap 的put()方法。

HashMap 的 key 是惟一的,由源碼能夠看出 HashSet 添加進去的值就是做爲HashMap 的key,而且在HashMap中若是K/V相同時,會用新的V覆蓋掉舊的V,而後返回舊的V。因此不會重複( HashMap 比較key是否相等是先比較hashcode 再比較equals )。

如下是HashSet 部分源碼:

private static final Object PRESENT = new Object();
private transient HashMap<E,Object> map;

public HashSet() {
    map = new HashMap<>();
}

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

hashCode()與equals()的相關規定:

  • 若是兩個對象相等,則hashcode必定也是相同的
  • 兩個對象相等,對兩個equals方法返回true
  • 兩個對象有相同的hashcode值,它們也不必定是相等的
  • 綜上,equals方法被覆蓋過,則hashCode方法也必須被覆蓋
  • hashCode()的默認行爲是對堆上的對象產生獨特值。若是沒有重寫hashCode(),則該class的兩個對象不管如何都不會相等(即便這兩個對象指向相同的數據)。

深刻理解可參考:Java中hashCode() 和 equals()的問題解答

1.6 HashSet與HashMap的區別

若是你看過 HashSet 源碼的話就應該知道:HashSet 底層就是基於 HashMap 實現的。(HashSet 的 源碼很是很是少,由於除了 clone() 、 writeObject() 、 readObject() 是 HashSet 本身不得不 實現以外,其餘方法都是直接調用 HashMap 中的方法。

HashMap HashSet
實現了Map接口 實現Set接口
存儲鍵值對 僅存儲對象
調用put()向map中添加元素 調用add()方法向Set中添加元素
HashMap使用鍵(Key)計算Hashcode HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,若是兩個對象不一樣的話,那麼返回false
HashMap相對於HashSet較快,由於它是使用惟一的鍵獲取對象 HashSet較HashMap來講比較慢

1.7 HashMap在JDK1.7和JDK1.8中有哪些不一樣?HashMap的底層實現

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

JDK1.8以前

JDK1.8 以前 HashMap 底層是 數組和鏈表 結合在一塊兒使用也就是 鏈表散列即採用的是拉鍊法。拉鍊法:將鏈表和數組相結合。也就是說建立一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希衝突,則將衝突的值加到鏈表中便可。

jdk1.7中HashMap數據結構

JDK1.8以後

相比於以前的版本,jdk1.8在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。

jdk1.8中HashMap數據結構

JDK1.7 VS JDK1.8 比較

JDK1.8主要解決或優化了一下問題:

  • resize 擴容優化
  • 引入了紅黑樹,目的是避免單條鏈表過長而影響查詢效率,紅黑樹算法請參考
  • 解決了多線程死循環問題,但還是非線程安全的,多線程時可能會形成數據丟失問題。

1.8 HashMap的put方法的具體流程?

當咱們put的時候,首先計算 key的hash值,這裏調用了 hash方法,hash方法實際是讓key.hashCode()與key.hashCode()>>>16進行異或操做,高16bit補0,一個數和0異或不變,因此 hash 函數大概的做用就是:高16bit不變,低16bit和高16bit作了一個異或,目的是減小碰撞。按照函數註釋,由於bucket數組大小是2的冪,計算下標index = (table.length - 1) & hash,若是不作 hash 處理,至關於散列生效的只有幾個低 bit 位,爲了減小散列的碰撞,設計者綜合考慮了速度、做用、質量以後,使用高16bit和低16bit異或來簡單處理減小碰撞,並且JDK8中用了複雜度 O(logn)的樹結構來提高碰撞下的性能。

putVal方法執行流程圖

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//實現Map.put和相關方法
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爲空則建立 
    // table未初始化或者長度爲0,進行擴容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 步驟②:計算index,並對null作處理  
    // (n - 1) & hash 肯定元素存放在哪一個桶中,桶爲空,新生成結點放入桶中(此時,這個結點是放在數組中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
        // 桶中已經存在元素
    else {
        Node<K,V> e; K k;
        // 步驟③:節點key存在,直接覆蓋value 
        // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 將第一個元素賦值給e,用e來記錄
            e = p;
            // 步驟④:判斷該鏈爲紅黑樹 
            // hash值不相等,即key不相等;爲紅黑樹結點
            // 若是當前元素類型爲TreeNode,表示爲紅黑樹,putTreeVal返回待存放的node, e可能爲null
        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);
                    //判斷鏈表的長度是否達到轉化紅黑樹的臨界值,臨界值爲8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //鏈表結構轉樹形結構
                        treeifyBin(tab, hash);
                    // 跳出循環
                    break;
                }
                // 判斷鏈表中結點的key值與插入的元素的key值是否相等
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循環
                    break;
                // 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表
                p = e;
            }
        }
        //判斷當前的key已經存在的狀況下,再來一個相同的hash值、key值時,返回新來的value這個值
        if (e != null) {
            // 記錄e的value
            V oldValue = e.value;
            // onlyIfAbsent爲false或者舊值爲null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替換舊值
                e.value = value;
            // 訪問後回調
            afterNodeAccess(e);
            // 返回舊值
            return oldValue;
        }
    }
    // 結構性修改
    ++modCount;
    // 步驟⑥:超過最大容量就擴容 
    // 實際大小大於閾值則擴容
    if (++size > threshold)
        resize();
    // 插入後回調
    afterNodeInsertion(evict);
    return null;
}

①.判斷鍵值對數組table[i]是否爲空或爲null,不然執行resize()進行擴容;

②.根據鍵值key計算hash值獲得插入的數組索引i,若是table[i]==null,直接新建節點添加,轉向⑥,若是table[i]不爲空,轉向③;

③.判斷table[i]的首個元素是否和key同樣,若是相同直接覆蓋value,不然轉向④,這裏的相同指的是hashCode以及equals;

④.判斷table[i] 是否爲treeNode,即table[i] 是不是紅黑樹,若是是紅黑樹,則直接在樹中插入鍵值對,不然轉向⑤;

⑤.遍歷table[i],判斷鏈表長度是否大於8,大於8的話把鏈表轉換爲紅黑樹,在紅黑樹中執行插入操做,不然進行鏈表的插入操做;遍歷過程當中若發現key已經存在直接覆蓋value便可;

⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,若是超過,進行擴容。

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

①.在jdk1.8中,resize方法是在hashmap中的鍵值對大於閥值時或者初始化時,就調用resize方法進行擴容;

②.每次擴展的時候,都是擴展2倍;

③.擴展後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。

在putVal()中,咱們看到在這個函數裏面使用到了2次resize()方法,resize()方法表示的在進行第一次初始化時會對其進行擴容,或者當該數組的實際大小大於其臨界值值(第一次爲12),這個時候在擴容的同時也會伴隨的桶上面的元素進行從新分發,這也是JDK1.8版本的一個優化的地方,在1.7中,擴容以後須要從新去計算其Hash值,根據Hash值對其進行分發,但在1.8版本中,則是根據在同一個桶的位置中進行判斷(e.hash & oldCap)是否爲0,從新進行hash分配後,該元素的位置要麼停留在原始位置,要麼移動到原始位置+增長的數組大小這個位置上

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//oldTab指向hash桶數組
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {//若是oldCap不爲空的話,就是hash桶數組不爲空
        if (oldCap >= MAXIMUM_CAPACITY) {//若是大於最大容量了,就賦值爲整數最大的閥值
            threshold = Integer.MAX_VALUE;
            return oldTab;//返回
        }//若是當前hash桶數組的長度在擴容後仍然小於最大容量 而且oldCap大於默認值16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold 雙倍擴容閥值threshold
    }
    // 舊的容量爲0,但threshold大於零,表明有參構造有cap傳入,threshold已經被初始化成最小2的n次冪
    // 直接將該值賦給新的容量
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
        // 無參構造建立的map,給出默認容量和threshold 16, 16*0.75
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 新的threshold = 新的cap * 0.75
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    // 計算出新的數組長度後賦給當前成員變量table
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶數組
    table = newTab;//將新數組的值複製給舊的hash桶數組
    // 若是原先的數組沒有初始化,那麼resize的初始化工做到此結束,不然進入擴容元素重排邏輯,使其均勻的分散
    if (oldTab != null) {
        // 遍歷新數組的全部桶下標
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 舊數組的桶下標賦給臨時變量e,而且解除舊數組中的引用,不然就數組沒法被GC回收
                oldTab[j] = null;
                // 若是e.next==null,表明桶中就一個元素,不存在鏈表或者紅黑樹
                if (e.next == null)
                    // 用一樣的hash映射算法把該元素加入新的數組
                    newTab[e.hash & (newCap - 1)] = e;
                    // 若是e是TreeNode而且e.next!=null,那麼處理樹中元素的重排
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    // e是鏈表的頭而且e.next!=null,那麼處理鏈表中元素重排
                else { // preserve order
                    // loHead,loTail 表明擴容後不用變換下標,見注1
                    Node<K,V> loHead = null, loTail = null;
                    // hiHead,hiTail 表明擴容後變換下標,見注1
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍歷鏈表
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                // 初始化head指向鏈表當前元素e,e不必定是鏈表的第一個元素,初始化後loHead
                                // 表明下標保持不變的鏈表的頭元素
                                loHead = e;
                            else
                                // loTail.next指向當前e
                                loTail.next = e;
                            // loTail指向當前的元素e
                            // 初始化後,loTail和loHead指向相同的內存,因此當loTail.next指向下一個元素時,
                            // 底層數組中的元素的next引用也相應發生變化,形成lowHead.next.next.....
                            // 跟隨loTail同步,使得lowHead能夠連接到全部屬於該鏈表的元素。
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                // 初始化head指向鏈表當前元素e, 初始化後hiHead表明下標更改的鏈表頭元素
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 遍歷結束, 將tail指向null,並把鏈表頭放入新數組的相應下標,造成新的映射。
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

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

什麼是哈希?

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

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

什麼是哈希衝突?

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

這樣咱們就能夠將擁有相同哈希值的對象組織成一個鏈表放在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是使用了哪些方法來有效解決哈希衝突的:

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

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

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

那怎麼解決呢?

  • HashMap本身實現了本身的hash()方法,經過兩次擾動使得它本身的哈希值高低位自行進行異或運算,下降哈希碰撞機率也使得數據分佈更平均;
  • 在保證數組長度爲2的冪次方的時候,使用hash()運算以後的值與運算(&)(數組長度 - 1)來獲取數組下標的方式進行存儲,這樣一來是比取餘操做更加有效率,二來也是由於只有當數組長度爲2的冪次方時,h&(length-1)纔等價於h%length,三來解決了「哈希值與數組大小範圍不匹配」的問題;

1.12 HashMap 的長度爲何是2的冪次方

爲了能讓 HashMap 存取高效,儘可能較少碰撞,也就是要儘可能把數據分配均勻,每一個鏈表/紅黑樹長度大體相同。這個實現就是把數據存到哪一個鏈表/紅黑樹中的算法。

這個算法應該如何設計呢?

咱們首先可能會想到採用%取餘的操做來實現。可是,重點來了:「取餘(%)操做中若是除數是2的冪次則等價於與其除數減一的與(&)操做(也就是說 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。」 而且 採用二進制位操做 &,相對於%可以提升運算效率,這就解釋了 HashMap 的長度爲何是2的冪次方。

那爲何是兩次擾動呢?

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

1.13 HashMap 與 HashTable 有什麼區別?

  • 線程安全HashMap 是非線程安全的,HashTable 是線程安全的;HashTable 內部的方法基本都通過 synchronized 修飾。(多線程可使用 ConcurrentHashMap 吧!);
  • 效率:由於線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable 基本被淘汰,不要在代碼中使用它;對Null key 和Null value的支持:HashMap 中,null 能夠做爲鍵,這樣的鍵只有一個,能夠有一個或多個鍵所對應的值爲 null。可是在 HashTable 中 put 進的鍵值只要有一個 null,直接拋NullPointerException。
  • 初始容量大小和每次擴充容量大小的不一樣 :①建立時若是不指定容量初始值,Hashtable 默認的初始大小爲11,以後每次擴充,容量變爲原來的2n+1。HashMap 默認的初始化大小爲16。以後每次擴充,容量變爲原來的2倍。②建立時若是給定了容量初始值,那麼 Hashtable 會直接使用你給定的大小,而 HashMap 會將其擴充爲2的冪次方大小。也就是說 HashMap 老是使用2的冪做爲哈希表的大小。底層數據結構:JDK1.8 之後的 HashMap 在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間。Hashtable 沒有這樣的機制。

1.14 HashMap 和 ConcurrentHashMap 的區別

  • ConcurrentHashMap對整個桶數組進行了分割分段(Segment),而後在每個分段上都用lock鎖進行保護,相對於HashTable的synchronized鎖的粒度更精細了一些,併發性能更好,而HashMap沒有鎖機制,不是線程安全的。(JDK1.8以後ConcurrentHashMap啓用了一種全新的方式實現,利用CAS算法。)
  • HashMap的鍵值對容許有null,可是ConCurrentHashMap都不容許。

1.15 ConcurrentHashMap 和 Hashtable 的區別?

ConcurrentHashMap 和 Hashtable 的區別主要體如今實現線程安全的方式上不一樣。

  • 「底層數據結構」:JDK1.7的 ConcurrentHashMap 底層採用 分段的數組+鏈表 實現,JDK1.8 採用的數據結構跟HashMap1.8的結構同樣,數組+鏈表/紅黑二叉樹。Hashtable 和 JDK1.8 以前的 HashMap 的底層數據結構相似都是採用 數組+鏈表 的形式,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的;
  • 「實現線程安全的方式」(重要):① 在JDK1.7的時候,ConcurrentHashMap(分段鎖) 對整個桶數組進行了分割分段(Segment),每一把鎖只鎖容器其中一部分數據,多線程訪問容器裏不一樣數據段的數據,就不會存在鎖競爭,提升併發訪問率。(默認分配16個Segment,比Hashtable效率提升16倍。) 到了 JDK1.8 的時候已經摒棄了Segment的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,併發控制使用 synchronized 和 CAS 來操做。(JDK1.6之後 對 synchronized鎖作了不少優化) 整個看起來就像是優化過且線程安全的 HashMap,雖然在JDK1.8中還能看到 Segment 的數據結構,可是已經簡化了屬性,只是爲了兼容舊版本;② Hashtable(同一把鎖) :使用 synchronized 來保證線程安全,效率很是低下。當一個線程訪問同步方法時,其餘線程也訪問同步方法,可能會進入阻塞或輪詢狀態,如使用 put 添加元素,另外一個線程不能使用 put 添加元素,也不能使用 get,競爭會愈來愈激烈效率越低。

「二者的對比圖」

HashTable:

JDK1.7的ConcurrentHashMap:

JDK1.8的ConcurrentHashMap(TreeBin: 紅黑二叉樹節點 Node: 鏈表節點):

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

1.16 ConcurrentHashMap 底層具體實現知道嗎?實現原理是什麼?

JDK1.7

首先將數據分爲一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據時,其餘段的數據也能被其餘線程訪問。

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

一個 ConcurrentHashMap 裏包含一個 Segment 數組。Segment 的結構和HashMap相似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每一個 HashEntry 是一個鏈表結構的元素,每一個 Segment 守護着一個HashEntry數組裏的元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment的鎖。

  • 該類包含兩個靜態內部類 HashEntry 和 Segment ;前者用來封裝映射表的鍵值對,後者用來充當鎖的角色;
  • Segment 是一種可重入的鎖 ReentrantLock,每一個 Segment 守護一個HashEntry 數組裏得元素,當對 HashEntry 數組的數據進行修改時,必須首先得到對應的 Segment 鎖。

JDK1.8

在JDK1.8中,放棄了Segment臃腫的設計,取而代之的是採用Node + CAS + Synchronized來保證併發安全進行實現,synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提高N倍。

結構以下:


1.17Array 和 ArrayList 有何區別?

  • Array 能夠存儲基本數據類型和對象,ArrayList 只能存儲對象。
  • Array 是指定固定大小的,而 ArrayList 大小是自動擴展的。
  • Array 內置方法沒有 ArrayList 多,好比 addAll、removeAll、iteration 等方法只有 ArrayList 有。

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

1.18 如何實現 Array 和 List 之間的轉換?

  • Array 轉 List:Arrays. asList(array) ;
  • List 轉 Array:List 的 toArray() 方法。

1.19 comparable 和 comparator的區別?

  • comparable接口其實是出自java.lang包,它有一個 compareTo(Object obj)方法用來排序
  • comparator接口其實是出自 java.util 包,它有一個compare(Object obj1, Object obj2)方法用來排序

通常咱們須要對一個集合使用自定義排序時,咱們就要重寫compareTo方法或compare方法,當咱們須要對某一個集合實現兩種排序方式,好比一個song對象中的歌名和歌手名分別採用一種排序方法的話,咱們能夠重寫compareTo方法和使用自制的Comparator方法或者以兩個Comparator來實現歌名排序和歌星名排序,第二種表明咱們只能使用兩個參數版的Collections.sort()。

Comparator定製排序

ArrayList<Integer> arrayList = new ArrayList<Integer>(); arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始數組:"); System.out.println(arrayList);
// void reverse(List list):反轉 Collections.reverse(arrayList); System.out.println("Collections.reverse(arrayList):"); System.out.println(arrayList);
// void sort(List list),按天然排序的升序排序 Collections.sort(arrayList); System.out.println("Collections.sort(arrayList):"); System.out.println(arrayList);
// 定製排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) { return o2.compareTo(o1);
        } });
System.out.println("定製排序後:"); System.out.println(arrayList);
原始數組:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1] 
Collections.sort(arrayList): 
[-9, -7, -5, -1, 3, 3, 4, 7] 
定製排序後:
[7, 4, 3, 3, -1, -5, -7, -9]

重寫compareTo方法實現按年齡來排序

// person對象沒有實現Comparable接口,因此必須實現,這樣纔不會出錯,纔可 以使treemap中的數據按順序排列
// 前面一個例子的String類已經默認實現了Comparable接口,詳細能夠查看 String類的API文檔,另外其餘
// 像Integer類等都已經實現了Comparable接口,因此不須要另外實現了
public  class Person implements Comparable<Person> {
    private String name;
    private int age;
    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) { this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) { this.age = age;
    }
    /**
     * TODO重寫compareTo方法實現按年齡來排序
     */
    @Override
    public int compareTo(Person o) {
        
      if (this.age > o.getAge()) {
          return 1;
       } else if (this.age < o.getAge()) {
          return -1; 
       }
        return age; 
    }
}
public static void main(String[] args) {
        TreeMap<Person, String> pdata = new TreeMap<Person, String>(); pdata.put(new Person("張三", 30), "zhangsan");
        pdata.put(new Person("李四", 20), "lisi");
        pdata.put(new Person("王五", 10), "wangwu");
        pdata.put(new Person("小紅", 5), "xiaohong");
// 獲得key的值的同時獲得key所對應的值
        Set<Person> keys = pdata.keySet();
        for (Person key : keys) {
        System.out.println(key.getAge() + "-" + key.getName()); }
}

5-小紅 10-王五 20-李四 30-張三

1.20 Collection 和 Collections 有什麼區別?

  • java.util.Collection 是一個集合接口(集合類的一個頂級接口)。它提供了對集合對象進行基本操做的通用接口方法。Collection接口在Java 類庫中有不少具體的實現。Collection接口的意義是爲各類具體的集合提供了最大化的統一操做方式,其直接繼承接口有List與Set。
  • Collections則是集合類的一個工具類/幫助類,其中提供了一系列靜態方法,用於對集合中元素進行排序、搜索以及線程安全等各類操做。

1.21 TreeMap 和 TreeSet 在排序時如何比較元素?Collections 工具類中的 sort()方法如何比較元素?

TreeSet 要求存放的對象所屬的類必須實現 Comparable 接口,該接口提供了比較元素的 compareTo()方法,當插入元素時會回調該方法比較元素的大小。TreeMap 要求存放的鍵值對映射的鍵必須實現 Comparable 接口從而根據鍵對元素進 行排 序。

Collections 工具類的 sort 方法有兩種重載的形式,

第一種要求傳入的待排序容器中存放的對象比較實現 Comparable 接口以實現元素的比較;

第二種不強制性的要求容器中的元素必須可比較,可是要求傳入第二個參數,參數是Comparator 接口的子類型(須要重寫 compare 方法實現元素的比較),至關於一個臨時定義的排序規則,其實就是經過接口注入比較元素大小的算法,也是對回調模式的應用(Java 中對函數式編程的支持)。

文章參考:

Java 8系列之從新認識HashMap

Java集合容器面試題

看到這裏今天的分享就結束了,若是以爲這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~

歡迎關注我的公衆號 「JavaClub」,按期爲你分享一些面試乾貨。

相關文章
相關標籤/搜索