一文搞懂全部Java集合面試題

Java集合

剛剛經歷過秋招,看了大量的面經,順便將常見的Java集合常考知識點總結了一下,並根據被問到的頻率大體作了一個標註。一顆星表示知識點須要瞭解,被問到的頻率不高,面試時起碼能說個差很少。兩顆星表示被問到的頻率較高或對理解Java有着重要的做用,建議熟練掌握。三顆星表示被問到的頻率很是高,建議深刻理解並熟練掌握其相關知識,方便麪試時拓展(方便裝逼),給面試官留下個好印象。html

推薦閱讀:一文搞懂全部Java基礎知識面試題java

經常使用的集合類有哪些? ***

Map接口和Collection接口是全部集合框架的父接口。下圖中的實線和虛線看着有些亂,其中接口與接口之間若是有聯繫爲繼承關係,類與類之間若是有聯繫爲繼承關係,類與接口之間則是類實現接口。重點掌握的抽象類有HashMapLinkedListHashTableArrayListHashSetStackTreeSetTreeMap。注意:Collection接口不是Map的父接口。面試

在這裏插入圖片描述

在這裏插入圖片描述

List,Set,Map三者的區別? ***

  • List有序集合(有序指存入的順序和取出的順序相同,不是按照元素的某些特性排序),可存儲重複元素,可存儲多個null
  • Set無序集合(元素存入和取出順序不必定相同),不可存儲重複元素,只能存儲一個null
  • Map:使用鍵值對的方式對元素進行存儲,key是無序的,且是惟一的。value值不惟一。不一樣的key值能夠對應相同的value值。

經常使用集合框架底層數據結構 ***

  • List:算法

    1. ArrayList:數組
    2. LinkedList:雙線鏈表
  • Setshell

    1. HashSet:底層基於HashMap實現,HashSet存入讀取元素的方式和HashMap中的Key是一致的。
    2. TreeSet:紅黑樹
  • Map數組

    1. HashMap: JDK1.8以前HashMap由數組+鏈表組成的, JDK1.8以後有數組+鏈表/紅黑樹組成,當鏈表長度大於8時,鏈表轉化爲紅黑樹,當長度小於6時,從紅黑樹轉化爲鏈表。這樣作的目的是能提升HashMap的性能,由於紅黑樹的查找元素的時間複雜度遠小於鏈表。
    2. HashTable:數組+鏈表
    3. TreeMap:紅黑樹

哪些集合類是線程安全的? ***

  • Vector:至關於有同步機制的ArrayList
  • Stack:棧
  • HashTable
  • enumeration:枚舉

迭代器 Iterator 是什麼 *

Iterator 是 Java 迭代器最簡單的實現,它不是一個集合,它是一種用於訪問集合的方法,Iterator接口提供遍歷任何Collection的接口。緩存

Java集合的快速失敗機制 「fail-fast」和安全失敗機制「fail-safe」是什麼? ***

  • 快速失敗安全

    Java的快速失敗機制是Java集合框架中的一種錯誤檢測機制,當多個線程同時對集合中的內容進行修改時可能就會拋出ConcurrentModificationException異常。其實不只僅是在多線程狀態下,在單線程中用加強for循環中一邊遍歷集合一邊修改集合的元素也會拋出ConcurrentModificationException異常。看下面代碼數據結構

    public class Main{
        public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
            for(Integer i : list){
                list.remove(i);  //運行時拋出ConcurrentModificationException異常
            }
        }
    }

    正確的作法是用迭代器的remove()方法,即可正常運行。多線程

    public class Main{
        public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Iterator<Integer> it = list.iterator();
            while(it.hasNext()){
                it.remove();
            }
        }
    }

    形成這種狀況的緣由是什麼?細心的同窗可能已經發現兩次調用的remove()方法不一樣,一個帶參數據,一個不帶參數,這個後面再說,通過查看ArrayList源碼,找到了拋出異常的代碼

    final void checkForComodification() {
          if (modCount != expectedModCount)
          		throw new ConcurrentModificationException();
    }

    從上面代碼中能夠看到若是modCountexpectedModCount這兩個變量不相等就會拋出ConcurrentModificationException異常。那這兩個變量又是什麼呢?繼續看源碼

    protected transient int modCount = 0; //在AbstractList中定義的變量
    int expectedModCount = modCount;//在ArrayList中的內部類Itr中定義的變量

    從上面代碼能夠看到,modCount初始值爲0,而expectedModCount初始值等於modCount。也就是說在遍歷的時候直接調用集合的remove()方法會致使modCount不等於expectedModCount進而拋出ConcurrentModificationException異常,而使用迭代器的remove()方法則不會出現這種問題。那麼只能在看看remove()方法的源碼找找緣由了

    public E remove(int index) {
            rangeCheck(index);
    
            modCount++;
            E oldValue = elementData(index);
    
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }

    從上面代碼中能夠看到只有modCount++了,而expectedModCount沒有操做,當每一次迭代時,迭代器會比較expectedModCountmodCount的值是否相等,因此在調用remove()方法後,modCount不等於expectedModCount了,這時就了報ConcurrentModificationException異常。但用迭代器中remove()的方法爲何不拋異常呢?原來**迭代器調用的remove()方法和上面的remove()方法不是同一個!**迭代器調用的remove()方法長這樣:

    public void remove() {
                if (lastRet < 0)
                    throw new IllegalStateException();
                checkForComodification();
    
                try {
                    ArrayList.this.remove(lastRet);
                    cursor = lastRet;
                    lastRet = -1;
                    expectedModCount = modCount;    //這行代碼保證了expectedModCount和modCount是相等的
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }

    從上面代碼能夠看到expectedModCount = modCount,因此迭代器的remove()方法保證了expectedModCountmodCount是相等的,進而保證了在加強for循環中修改集合內容不會報ConcurrentModificationException異常。

    上面介紹的只是單線程的狀況,用迭代器調用remove()方法便可正常運行,但若是是多線程會怎麼樣呢?

    答案是在多線程的狀況下即便用了迭代器調用remove()方法,仍是會報ConcurrentModificationException異常。這又是爲何呢?仍是要從expectedModCountmodCount這兩個變量入手分析,剛剛說了modCountAbstractList類中定義,而expectedModCountArrayList內部類中定義,因此modCount是個共享變量而expectedModCount是屬於線程各自的。簡單說,線程1更新了modCount和屬於本身的expectedModCount,而在線程2看來只有modCount更新了,expectedModCount並未更新,因此會拋出ConcurrentModificationException異常。

  • 安全失敗

    採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。因此在遍歷過程當中對原集合所做的修改並不能被迭代器檢測到,因此不會拋出ConcurrentModificationException異常。缺點是迭代器遍歷的是開始遍歷那一刻拿到的集合拷貝,在遍歷期間原集合發生了修改,迭代器是沒法訪問到修改後的內容。java.util.concurrent包下的容器都是安全失敗,能夠在多線程下併發使用。

如何邊遍歷邊移除 Collection 中的元素? ***

從上文**「快速失敗機制」**可知在遍歷集合時若是直接調用remove()方法會拋出ConcurrentModificationException異常,因此使用迭代器中調用remove()方法。

Array 和 ArrayList 有何區別? ***

  • Array能夠包含基本類型和對象類型,ArrayList只能包含對象類型。
  • Array大小是固定的,ArrayList的大小是動態變化的。(ArrayList的擴容是個常見面試題)
  • 相比於ArrayArrayList有着更多的內置方法,如addAll()removeAll()
  • 對於基本類型數據,ArrayList 使用自動裝箱來減小編碼工做量;而當處理固定大小的基本數據類型的時候,這種方式相對比較慢,這時候應該使用Array

comparable 和 comparator的區別? ** 

  • comparable接口出自java.lang包,能夠理解爲一個內比較器,由於實現了Comparable接口的類能夠和本身比較,要和其餘實現了Comparable接口類比較,可使用compareTo(Object obj)方法。compareTo方法的返回值是int,有三種狀況:
    1. 返回正整數(比較者大於被比較者)
    2. 返回0(比較者等於被比較者)
    3. 返回負整數(比較者小於被比較者)
  • comparator接口出自 java.util 包,它有一個compare(Object obj1, Object obj2)方法用來排序,返回值一樣是int,有三種狀況,和compareTo相似。

它們之間的區別:不少包裝類都實現了comparable接口,像IntegerString等,因此直接調用Collections.sort()直接可使用。若是對類裏面自帶的天然排序不滿意,而又不能修改其源代碼的狀況下,使用Comparator就比較合適。此外使用Comparator能夠避免添加額外的代碼與咱們的目標類耦合,同時能夠定義多種排序規則,這一點是Comparable接口無法作到的,從靈活性和擴展性講Comparator更優,故在面對自定義排序的需求時,能夠優先考慮使用Comparator接口。

Collection 和 Collections 有什麼區別? **

  • Collection 是一個集合接口。它提供了對集合對象進行基本操做的通用接口方法。
  • Collections 是一個包裝類。它包含有各類有關集合操做的靜態多態方法,例如經常使用的sort()方法。此類不能實例化,就像一個工具類,服務於Java的Collection框架。

List集合

遍歷一個 List 有哪些不一樣的方式? **

先說一下常見的元素在內存中的存儲方式,主要有兩種:

  1. 順序存儲(Random Access):相鄰的數據元素在內存中的位置也是相鄰的,能夠根據元素的位置(如ArrayList中的下表)讀取元素。
  2. 鏈式存儲(Sequential Access):每一個數據元素包含它下一個元素的內存地址,在內存中不要求相鄰。例如LinkedList

主要的遍歷方式主要有三種:

  1. for循環遍歷:遍歷者本身在集合外部維護一個計數器,依次讀取每個位置的元素。
  2. Iterator遍歷:基於順序存儲集合的Iterator能夠直接按位置訪問數據。基於鏈式存儲集合的Iterator,須要保存當前遍歷的位置,而後根據當前位置來向前或者向後移動指針。
  3. foreach遍歷:foreach 內部也是採用了Iterator 的方式實現,但使用時不須要顯示地聲明Iterator

那麼對於以上三種遍歷方式應該如何選取呢?

在Java集合框架中,提供了一個RandomAccess接口,該接口沒有方法,只是一個標記。一般用來標記List的實現是否支持RandomAccess。因此在遍歷時,能夠先判斷是否支持RandomAccesslist instanceof RandomAccess),若是支持可用 for 循環遍歷,不然建議用Iteratorforeach 遍歷。

ArrayList的擴容機制 ***

先說下結論,通常面試時須要記住,ArrayList的初始容量爲10,擴容時對是舊的容量值加上舊的容量數值進行右移一位(位運算,至關於除以2,位運算的效率更高),因此每次擴容都是舊的容量的1.5倍。

具體的實現你們可查看下ArrayList的源碼。

ArrayList 和 LinkedList 的區別是什麼? ***

  • 是否線程安全:ArrayListLinkedList都是不保證線程安全的
  • 底層實現:ArrayList的底層實現是數組,LinkedList的底層是雙向鏈表。
  • 內存佔用:ArrayList會存在必定的空間浪費,由於每次擴容都是以前的1.5倍,而LinkedList中的每一個元素要存放直接後繼和直接前驅以及數據,因此對於每一個元素的存儲都要比ArrayList花費更多的空間。
  • 應用場景:ArrayList的底層數據結構是數組,因此在插入和刪除元素時的時間複雜度都會收到位置的影響,平均時間複雜度爲o(n),在讀取元素的時候能夠根據下標直接查找到元素,不受位置的影響,平均時間複雜度爲o(1),因此ArrayList更加適用於多讀,少增刪的場景LinkedList的底層數據結構是雙向鏈表,因此插入和刪除元素不受位置的影響,平均時間複雜度爲o(1),若是是在指定位置插入則是o(n),由於在插入以前須要先找到該位置,讀取元素的平均時間複雜度爲o(n)。因此LinkedList更加適用於多增刪,少讀寫的場景

ArrayList 和 Vector 的區別是什麼? ***

  • 相同點

    1. 都實現了List接口
    2. 底層數據結構都是數組
  • 不一樣點

    1. 線程安全:Vector使用了Synchronized來實現線程同步,因此是線程安全的,而ArrayList是線程不安全的。
    2. 性能:因爲Vector使用了Synchronized進行加鎖,因此性能不如ArrayList
    3. 擴容:ArrayListVector都會根據須要動態的調整容量,可是ArrayList每次擴容爲舊容量的1.5倍,而Vector每次擴容爲舊容量的2倍。

簡述 ArrayList、Vector、LinkedList 的存儲性能和特性? ***

  • ArrayList底層數據結構爲數組,對元素的讀取速度快,而增刪數據慢,線程不安全。
  • LinkedList底層爲雙向鏈表,對元素的增刪數據快,讀取慢,線程不安全。
  • Vector的底層數據結構爲數組,用Synchronized來保證線程安全,性能較差,但線程安全。

Set集合

說一下 HashSet 的實現原理 ***

HashSet的底層是HashMap,默認構造函數是構建一個初始容量爲16,負載因子爲0.75 的HashMapHashSet的值存放於HashMapkey上,HashMapvalue統一爲PRESENT

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

這裏面涉及到了HasCode()equals()兩個方法。

  • equals()

    先看下String類中重寫的equals方法。

    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String anotherString = (String)anObject;
                int n = value.length;
                if (n == anotherString.value.length) {
                    char v1[] = value;
                    char v2[] = anotherString.value;
                    int i = 0;
                    while (n-- != 0) {
                        if (v1[i] != v2[i])
                            return false;
                        i++;
                    }
                    return true;
                }
            }
            return false;
        }

    從源碼中能夠看到:

    1. equals方法首先比較的是內存地址,若是內存地址相同,直接返回true;若是內存地址不一樣,再比較對象的類型,類型不一樣直接返回false;類型相同,再比較值是否相同;值相同返回true,值不一樣返回false。總結一下,equals會比較內存地址、對象類型、以及值,內存地址相同,equals必定返回true;對象類型和值相同,equals方法必定返回true
    2. 若是沒有重寫equals方法,那麼equals==的做用相同,比較的是對象的地址值。
  • hashCode

    hashCode方法返回對象的散列碼,返回值是int類型的散列碼。散列碼的做用是肯定該對象在哈希表中的索引位置。

    關於hashCode有一些約定:

    1. 兩個對象相等,則hashCode必定相同。
    2. 兩個對象有相同的hashCode值,它們不必定相等。
    3. hashCode()方法默認是對堆上的對象產生獨特值,若是沒有重寫hashCode()方法,則該類的兩個對象的hashCode值確定不一樣

介紹完equals()方法和hashCode()方法,繼續說下HashSet是如何檢查重複的。

HashSet的特色是存儲元素時無序且惟一,在向HashSet中添加對象時,首相會計算對象的HashCode值來肯定對象的存儲位置,若是該位置沒有其餘對象,直接將該對象添加到該位置;若是該存儲位置有存儲其餘對象(新添加的對象和該存儲位置的對象的HashCode值相同),調用equals方法判斷兩個對象是否相同,若是相同,則添加對象失敗,若是不相同,則會將該對象從新散列到其餘位置。

HashSet與HashMap的區別 ***

HashMap HashSet
實現了Map接口 實現了Set接口
存儲鍵值對 存儲對象
key惟一,value不惟一 存儲對象惟一
HashMap使用鍵(Key)計算Hashcode HashSet使用成員對象來計算hashcode
速度相對較快 速度相對較慢

Map集合

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

  • JDK1.7的底層數據結構(數組+鏈表)

在這裏插入圖片描述

  • JDK1.8的底層數據結構(數組+鏈表)

在這裏插入圖片描述

  • JDK1.7的Hash函數

    static final int hash(int h){
    	h ^= (h >>> 20) ^ (h >>>12);
        return h^(h >>> 7) ^ (h >>> 4);
    }
  • JDK1.8的Hash函數

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

    JDK1.8的函數通過了一次異或一次位運算一共兩次擾動,而JDK1.7通過了四次位運算五次異或一共九次擾動。這裏簡單解釋下JDK1.8的hash函數,面試常常問這個,兩次擾動分別是key.hashCode()key.hashCode()右移16位進行異或。這樣作的目的是,高16位不變,低16位與高16位進行異或操做,進而減小碰撞的發生,高低Bit都參與到Hash的計算。如何不進行擾動處理,由於hash值有32位,直接對數組的長度求餘,起做用只是hash值的幾個低位。

HashMap在JDK1.7和JDK1.8中有哪些不一樣點:

JDK1.7 JDK1.8 JDK1.8的優點
底層結構 數組+鏈表 數組+鏈表/紅黑樹(鏈表大於8) 避免單條鏈表過長而影響查詢效率,提升查詢效率
hash值計算方式 9次擾動 = 4次位運算 + 5次異或運算 2次擾動 = 1次位運算 + 1次異或運算 能夠均勻地把以前的衝突的節點分散到新的桶(具體細節見下面擴容部分)
插入數據方式 頭插法(先講原位置的數據移到後1位,再插入數據到該位置) 尾插法(直接插入到鏈表尾部/紅黑樹) 解決多線程形成死循環地問題
擴容後存儲位置的計算方式 從新進行hash計算 原位置或原位置+舊容量 省去了從新計算hash值的時間

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

由於HashMap是經過key的hash值來肯定存儲的位置,但Hash值的範圍是-2147483648到2147483647,不可能創建一個這麼大的數組來覆蓋全部hash值。因此在計算完hash值後會對數組的長度進行取餘操做,若是數組的長度是2的冪次方,(length - 1)&hash等同於hash%length,能夠用(length - 1)&hash這種位運算來代替%取餘的操做進而提升性能。

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

HashMap的主要流程能夠看下面這個流程圖,邏輯很是清晰。

在這裏插入圖片描述

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

  • 初始值爲16,負載因子爲0.75,閾值爲負載因子*容量

  • resize()方法是在hashmap中的鍵值對大於閥值時或者初始化時,就調用resize()方法進行擴容。

  • 每次擴容,容量都是以前的兩倍

  • 擴容時有個判斷e.hash & oldCap是否爲零,也就是至關於hash值對數組長度的取餘操做,若等於0,則位置不變,若等於1,位置變爲原位置加舊容量。

    源碼以下:

    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) {
            if (oldCap >= MAXIMUM_CAPACITY) { //若是舊容量已經超過最大值,閾值爲整數最大值
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;  //沒有超過最大值就變爲原來的2倍
        }
        else if (oldThr > 0) 
            newCap = oldThr;
    
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    
        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) {
            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 { 
                        Node<K,V> loHead = null, loTail = null;//loHead,loTail 表明擴容後在原位置
                        Node<K,V> hiHead = null, hiTail = null;//hiHead,hiTail 表明擴容後在原位置+舊容量
                        Node<K,V> next;
                        do {             
                            next = e.next;
                            if ((e.hash & oldCap) == 0) { //判斷是否爲零,爲零賦值到loHead,不爲零賦值到hiHead
                                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;   //loHead放在原位置
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;  //hiHead放在原位置+舊容量
                        }
                    }
                }
            }
        }
        return newTab;
    }

HashMap默認加載因子爲何選擇0.75?

這個主要是考慮空間利用率和查詢成本的一個折中。若是加載因子太高,空間利用率提升,可是會使得哈希衝突的機率增長;若是加載因子太低,會頻繁擴容,哈希衝突機率下降,可是會使得空間利用率變低。具體爲何是0.75,不是0.74或0.76,這是一個基於數學分析(泊松分佈)和行業規定一塊兒獲得的一個結論。

爲何要將鏈表中轉紅黑樹的閾值設爲8?爲何不一開始直接使用紅黑樹?

可能有不少人會問,既然紅黑樹性能這麼好,爲何不一開始直接使用紅黑樹,而是先用鏈表,鏈表長度大於8時,才轉換爲紅紅黑樹。

  • 由於紅黑樹的節點所佔的空間是普通鏈表節點的兩倍,但查找的時間複雜度低,因此只有當節點特別多時,紅黑樹的優勢才能體現出來。至於爲何是8,是經過數據分析統計出來的一個結果,鏈表長度到達8的機率是很低的,綜合鏈表和紅黑樹的性能優缺點考慮將大於8的鏈表轉化爲紅黑樹。
  • 鏈表轉化爲紅黑樹除了鏈表長度大於8,還要HashMap中的數組長度大於64。也就是若是HashMap長度小於64,鏈表長度大於8是不會轉化爲紅黑樹的,而是直接擴容。

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

哈希衝突:hashMap在存儲元素時會先計算key的hash值來肯定存儲位置,由於key的hash值計算最後有個對數組長度取餘的操做,因此即便不一樣的key也可能計算出相同的hash值,這樣就引發了hash衝突。hashMap的底層結構中的鏈表/紅黑樹就是用來解決這個問題的。

HashMap中的哈希衝突解決方式能夠主要從三方面考慮(以JDK1.8爲背景)

  • 拉鍊法

    HasMap中的數據結構爲數組+鏈表/紅黑樹,當不一樣的key計算出的hash值相同時,就用鏈表的形式將Node結點(衝突的keykey對應的value)掛在數組後面。

  • hash函數

    key的hash值通過兩次擾動,keyhashCode值與keyhashCode值的右移16位進行異或,而後對數組的長度取餘(實際爲了提升性能用的是位運算,但目的和取餘同樣),這樣作可讓hashCode取值出的高位也參與運算,進一步下降hash衝突的機率,使得數據分佈更平均。

  • 紅黑樹

    在拉鍊法中,若是hash衝突特別嚴重,則會致使數組上掛的鏈表長度過長,性能變差,所以在鏈表長度大於8時,將鏈表轉化爲紅黑樹,能夠提升遍歷鏈表的速度。

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

hashCode()處理後的哈希值範圍太大,不可能在內存創建這麼大的數組。

可否使用任何類做爲 Map 的 key? ***

能夠,但要注意如下兩點:

  • 若是類重寫了 equals() 方法,也應該重寫hashCode()方法。
  • 最好定義key類是不可變的,這樣key對應的hashCode() 值能夠被緩存起來,性能更好,這也是爲何String特別適合做爲HashMapkey

爲何HashMap中String、Integer這樣的包裝類適合做爲Key? ***

  • 這些包裝類都是final修飾,是不可變性的, 保證了key的不可更改性,不會出現放入和獲取時哈希值不一樣的狀況。
  • 它們內部已經重寫過hashcode(),equal()等方法。

若是使用Object做爲HashMap的Key,應該怎麼辦呢? **

  • 重寫hashCode()方法,由於須要計算hash值肯定存儲位置
  • 重寫equals()方法,由於須要保證key的惟一性。

HashMap 多線程致使死循環問題 ***

因爲JDK1.7的hashMap遇到hash衝突採用的是頭插法,在多線程狀況下會存在死循環問題,但JDK1.8已經改爲了尾插法,不存在這個問題了。但須要注意的是JDK1.8中的HashMap仍然是不安全的,在多線程狀況下使用仍然會出現線程安全問題。基本上面試時說到這裏既能夠了,具體流程用口述是很難說清的,感興趣的能夠看這篇文章。HASHMAP的死循環

ConcurrentHashMap 底層具體實現知道嗎? **

  • JDK1.7

    在JDK1.7中,ConcurrentHashMap採用Segment數組 + HashEntry數組的方式進行實現。Segment實現了ReentrantLock,因此Segment有鎖的性質,HashEntry用於存儲鍵值對。一個ConcurrentHashMap包含着一個Segment數組,一個Segment包含着一個HashEntry數組,HashEntry是一個鏈表結構,若是要獲取HashEntry中的元素,要先得到Segment的鎖。

在這裏插入圖片描述

  • JDK1.8

    在JDK1.8中,不在是Segment+HashEntry的結構了,而是和HashMap相似的結構,Node數組+鏈表/紅黑樹,採用CAS+synchronized來保證線程安全。當鏈表長度大於8,鏈表轉化爲紅黑樹。在JDK1.8中synchronized只鎖鏈表或紅黑樹的頭節點,是一種相比於segment更爲細粒度的鎖,鎖的競爭變小,因此效率更高。

在這裏插入圖片描述

總結一下:

  • JDK1.7底層是ReentrantLock+Segment+HashEntry,JDK1.8底層是synchronized+CAS+鏈表/紅黑樹
  • JDK1.7採用的是分段鎖,同時鎖住幾個HashEntry,JDK1.8鎖的是Node節點,只要沒有發生哈希衝突,就不會產生鎖的競爭。因此JDK1.8相比於JDK1.7提供了一種粒度更小的鎖,減小了鎖的競爭,提升了ConcurrentHashMap的併發能力。

HashTable的底層實現知道嗎?

HashTable的底層數據結構是數組+鏈表,鏈表主要是爲了解決哈希衝突,而且整個數組都是synchronized修飾的,因此HashTable是線程安全的,但鎖的粒度太大,鎖的競爭很是激烈,效率很低。

在這裏插入圖片描述

HashMap、ConcurrentHashMap及Hashtable 的區別 ***

HashMap(JDK1.8) ConcurrentHashMap(JDK1.8) Hashtable
底層實現 數組+鏈表/紅黑樹 數組+鏈表/紅黑樹 數組+鏈表
線程安全 不安全 安全(Synchronized修飾Node節點) 安全(Synchronized修飾整個表)
效率 較高
擴容 初始16,每次擴容成2n 初始16,每次擴容成2n 初始11,每次擴容成2n+1
是否支持Null key和Null Value 能夠有一個Null key,Null Value多個 不支持 不支持

Java集合的經常使用方法 **

這些經常使用方法是須要背下來的,雖然面試用不上,可是筆試或者面試寫算法題時會常常用到。

Collection經常使用方法

方法
booean add(E e) 在集合末尾添加元素
boolean remove(Object o) 若本類集中有值與o的值相等的元素,移除該元素並返回true
void clear() 清除本類中全部元素
boolean contains(Object o) 判斷集合中是否包含該元素
boolean isEmpty() 判斷集合是否爲空
int size() 返回集合中元素的個數
boolean addAll(Collection c) 將一個集合中c中的全部元素添加到另外一個集合中
Object[] toArray() 返回一個包含本集全部元素的數組,數組類型爲Object[]
`boolean equals(Object c)`` 判斷元素是否相等
int hashCode() 返回元素的hash值

List特有方法

方法
void add(int index,Object obj) 在指定位置添加元素
Object remove(int index) 刪除指定元素並返回
Object set(int index,Object obj) 把指定索引位置的元素更改成指定值並返回修改前的值
int indexOf(Object o) 返回指定元素在集合中第一次出現的索引
Object get(int index) 返回指定位置的元素
List subList(int fromIndex,int toIndex) 截取集合(左閉右開)

LinkedList特有方法

方法
addFirst() 在頭部添加元素
addLast() 在尾部添加元素
removeFirst() 在頭部刪除元素
removeLat() 在尾部刪除元素
getFirst() 獲取頭部元素
getLast() 獲取尾部元素

Map

方法
void clear() 清除集合內的元素
boolean containsKey(Object key) 查詢Map中是否包含指定key,若是包含則返回true
Set entrySet() 返回Map中所包含的鍵值對所組成的Set集合,每一個集合元素都是Map.Entry的對象
Object get(Object key) 返回key指定的value,若Map中不包含key返回null
boolean isEmpty() 查詢Map是否爲空,若爲空返回true
Set keySet() 返回Map中全部key所組成的集合
Object put(Object key,Object value) 添加一個鍵值對,若是已有一個相同的key,則新的鍵值對會覆蓋舊的鍵值對,返回值爲覆蓋前的value值,不然爲null
void putAll(Map m) 將制定Map中的鍵值對複製到Map中
Object remove(Object key) 刪除指定key所對應的鍵值對,返回所關聯的value,若是key不存在返回null
int size() 返回Map裏面的鍵值對的個數
Collection values() 返回Map裏全部values所組成的Collection
boolean containsValue ( Object value) 判斷映像中是否存在值 value

Stack

方法
boolean empty() 測試堆棧是否爲空。
E peek() 查看堆棧頂部的對象,但不從堆棧中移除它。
E pop() 移除堆棧頂部的對象,並做爲此函數的值返回該對象。
E push(E item) 把項壓入堆棧頂部。
int search(Object o) 返回對象在堆棧中的位置,以 1 爲基數。

Queue

方法
boolean add(E e) 將指定元素插入到隊列的尾部(隊列滿了話,會拋出異常)
boolean offer(E e) 將指定元素插入此隊列的尾部(隊列滿了話,會返回false)
E remove() 返回取隊列頭部的元素,並刪除該元素(若是隊列爲空,則拋出異常)
E poll() 返回隊列頭部的元素,並刪除該元素(若是隊列爲空,則返回null)
E element() 返回隊列頭部的元素,不刪除該元素(若是隊列爲空,則拋出異常)
E peek() 返回隊列頭部的元素,不刪除該元素(若是隊列爲空,則返回null)
相關文章
相關標籤/搜索