全網閱讀過20k的Java集合框架常見面試題總結!

本文爲 SnailClimb 的原創,目前已經收錄自我開源的 JavaGuide 中(61.5 k Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所須要掌握的核心知識。歡迎 Star!)。html

文末有個人公衆號,公衆號裏有我最新整理的Java學習資料,免費分享。java

這麼好的文章,必定好先贊後看!!!建議養成這個好習慣!!愛大家!😍git

剖析面試最多見問題之Java集合框架

當了會標題黨,這是第一次,後面還有不少次!不過這文章全網閱讀確定是超過 20 k 的,並且通過了不少同行的優化,質量有保障哦!程序員

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

  • List(對付順序的好幫手): List接口存儲一組不惟一(能夠有多個元素引用相同的對象),有序的對象
  • Set(注重獨一無二的性質): 不容許重複的集合。不會有多個元素引用相同的對象。
  • Map(用Key來搜索的專家): 使用鍵值對存儲。Map會維護與Key有關聯的值。兩個Key能夠引用相同的對象,但Key不能重複,典型的Key是String類型,但也能夠是任何對象。

Arraylist 與 LinkedList 區別?

  • 1. 是否保證線程安全: ArrayListLinkedList 都是不一樣步的,也就是不保證線程安全;github

  • 2. 底層數據結構: Arraylist 底層使用的是 Object 數組LinkedList 底層使用的是 雙向鏈表 數據結構(JDK1.6以前爲循環鏈表,JDK1.7取消了循環。注意雙向鏈表和雙向循環鏈表的區別,下面有介紹到!)面試

  • 3. 插入和刪除是否受元素位置的影響: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))由於須要先移動到指定位置再插入。算法

  • 4. 是否支持快速隨機訪問: LinkedList 不支持高效的隨機元素訪問,而 ArrayList 支持。快速隨機訪問就是經過元素的序號快速獲取元素對象(對應於get(int index)方法)。spring

  • 5. 內存空間佔用: ArrayList的空 間浪費主要體如今在list列表的結尾會預留必定的容量空間,而LinkedList的空間花費則體如今它的每個元素都須要消耗比ArrayList更多的空間(由於要存放直接後繼和直接前驅以及數據)。shell

補充內容:RandomAccess接口

public interface RandomAccess {
}

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

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

public static <T>
    int binarySearch(List<? extends Comparable<? super T>> 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循環

補充內容:雙向鏈表和雙向循環鏈表

雙向鏈表: 包含兩個指針,一個prev指向前一個節點,一個next指向後一個節點。

雙向鏈表

雙向循環鏈表: 最後一個節點的 next 指向head,而 head 的prev指向最後一個節點,構成一個環。

雙向循環鏈表

ArrayList 與 Vector 區別呢?爲何要用Arraylist取代Vector呢?

Vector類的全部方法都是同步的。能夠由兩個線程安全地訪問一個Vector對象、可是一個線程訪問Vector的話代碼要在同步操做上耗費大量的時間。

Arraylist不是同步的,因此在不須要保證線程安全時建議使用Arraylist。

說一說 ArrayList 的擴容機制吧

詳見筆主的這篇文章:經過源碼一步一步分析ArrayList 擴容機制

HashMap 和 Hashtable 的區別

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

HashMap 中帶有初始容量的構造函數:

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

下面這個方法保證了 HashMap 老是使用2的冪做爲哈希表的大小。

/**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap 和 HashSet區別

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

HashMap HashSet
實現了Map接口 實現Set接口
存儲鍵值對 僅存儲對象
調用 put()向map中添加元素 調用 add()方法向Set中添加元素
HashMap使用鍵(Key)計算Hashcode HashSet使用成員對象來計算hashcode值,對於兩個對象來講hashcode可能相同,因此equals()方法用來判斷對象的相等性,

HashSet如何檢查重複

當你把對象加入HashSet時,HashSet會先計算對象的hashcode值來判斷對象加入的位置,同時也會與其餘加入的對象的hashcode值做比較,若是沒有相符的hashcode,HashSet會假設對象沒有重複出現。可是若是發現有相同hashcode值的對象,這時會調用equals()方法來檢查hashcode相等的對象是否真的相同。若是二者相同,HashSet就不會讓加入操做成功。(摘自個人Java啓蒙書《Head fist java》第二版)

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

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

==與equals的區別

  1. ==是判斷兩個變量或實例是否是指向同一個內存空間 equals是判斷兩個變量或實例所指向的內存空間的值是否是相同
  2. ==是指對內存地址進行比較 equals()是對字符串的內容進行比較
  3. ==指引用是否相同 equals()指的是值是否相同

HashMap的底層實現

JDK1.8以前

JDK1.8 以前 HashMap 底層是 數組和鏈表 結合在一塊兒使用也就是 鏈表散列HashMap 經過 key 的 hashCode 通過擾動函數處理事後獲得 hash 值,而後經過 (n - 1) & hash 判斷當前元素存放的位置(這裏的 n 指的是數組的長度),若是當前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,若是相同的話,直接覆蓋,不相同就經過拉鍊法解決衝突。

所謂擾動函數指的就是 HashMap 的 hash 方法。使用 hash 方法也就是擾動函數是爲了防止一些實現比較差的 hashCode() 方法 換句話說使用擾動函數以後能夠減小碰撞。

JDK 1.8 HashMap 的 hash 方法源碼:

JDK 1.8 的 hash方法 相比於 JDK 1.7 hash 方法更加簡化,可是原理不變。

static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^ :按位異或
      // >>>:無符號右移,忽略符號位,空位都以0補齊
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

對比一下 JDK1.7的 HashMap 的 hash 方法源碼.

static int hash(int h) {
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

相比於 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能會稍差一點點,由於畢竟擾動了 4 次。

所謂 「拉鍊法」 就是:將鏈表和數組相結合。也就是說建立一個鏈表數組,數組中每一格就是一個鏈表。若遇到哈希衝突,則將衝突的值加到鏈表中便可。

jdk1.8以前的內部結構-HashMap

JDK1.8以後

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

jdk1.8以後的內部結構-HashMap

TreeMap、TreeSet以及JDK1.8以後的HashMap底層都用到了紅黑樹。紅黑樹就是爲了解決二叉查找樹的缺陷,由於二叉查找樹在某些狀況下會退化成一個線性結構。

推薦閱讀:

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

爲了能讓 HashMap 存取高效,儘可能較少碰撞,也就是要儘可能把數據分配均勻。咱們上面也講到了過了,Hash 值的範圍值-2147483648到2147483647,先後加起來大概40億的映射空間,只要哈希函數映射得比較均勻鬆散,通常應用是很難出現碰撞的。但問題是一個40億長度的數組,內存是放不下的。因此這個散列值是不能直接拿來用的。用以前還要先作對數組的長度取模運算,獲得的餘數才能用來要存放的位置也就是對應的數組下標。這個數組下標的計算方法是「 (n - 1) & hash」。(n表明數組長度)。這也就解釋了 HashMap 的長度爲何是2的冪次方。

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

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

HashMap 多線程操做致使死循環問題

主要緣由在於 併發下的Rehash 會形成元素之間會造成一個循環鏈表。不過,jdk 1.8 後解決了這個問題,可是仍是不建議在多線程下使用 HashMap,由於多線程下使用 HashMap 仍是會存在其餘問題好比數據丟失。併發環境下推薦使用 ConcurrentHashMap 。

詳情請查看:https://coolshell.cn/articles/9606.html

ConcurrentHashMap 和 Hashtable 的區別

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

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

二者的對比圖:

圖片來源:http://www.cnblogs.com/chengxiao/p/6842045.html

HashTable:

HashTable全表鎖

JDK1.7的ConcurrentHashMap:

JDK1.7的ConcurrentHashMap

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

JDK1.8的ConcurrentHashMap

ConcurrentHashMap線程安全的具體實現方式/底層具體實現

JDK1.7(上面有示意圖)

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

ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成

Segment 實現了 ReentrantLock,因此 Segment 是一種可重入鎖,扮演鎖的角色。HashEntry 用於存儲鍵值對數據。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

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

JDK1.8 (上面有示意圖)

ConcurrentHashMap取消了Segment分段鎖,採用CAS和synchronized來保證併發安全。數據結構跟HashMap1.8的結構相似,數組+鏈表/紅黑二叉樹。Java 8在鏈表長度超過必定閾值(8)時將鏈表(尋址時間複雜度爲O(N))轉換爲紅黑樹(尋址時間複雜度爲O(log(N)))

synchronized只鎖定當前鏈表或紅黑二叉樹的首節點,這樣只要hash不衝突,就不會產生併發,效率又提高N倍。

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);

Output:

原始數組:
[-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) {
        // TODO Auto-generated method stub
        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());

        }
    }

Output:

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

集合框架底層數據結構總結

Collection

1. List

  • Arraylist: Object數組
  • Vector: Object數組
  • LinkedList: 雙向鏈表(JDK1.6以前爲循環鏈表,JDK1.7取消了循環)

2. Set

  • HashSet(無序,惟一): 基於 HashMap 實現的,底層採用 HashMap 來保存元素
  • LinkedHashSet: LinkedHashSet 繼承於 HashSet,而且其內部是經過 LinkedHashMap 來實現的。有點相似於咱們以前說的LinkedHashMap 其內部是基於 HashMap 實現同樣,不過仍是有一點點區別的
  • TreeSet(有序,惟一): 紅黑樹(自平衡的排序二叉樹)

Map

  • HashMap: JDK1.8以前HashMap由數組+鏈表組成的,數組是HashMap的主體,鏈表則是主要爲了解決哈希衝突而存在的(「拉鍊法」解決衝突)。JDK1.8之後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲8)時,將鏈表轉化爲紅黑樹,以減小搜索時間
  • LinkedHashMap: LinkedHashMap 繼承自 HashMap,因此它的底層仍然是基於拉鍊式散列結構即由數組和鏈表或紅黑樹組成。另外,LinkedHashMap 在上面結構的基礎上,增長了一條雙向鏈表,使得上面的結構能夠保持鍵值對的插入順序。同時經過對鏈表進行相應的操做,實現了訪問順序相關邏輯。詳細能夠查看:《LinkedHashMap 源碼詳細分析(JDK1.8)》
  • Hashtable: 數組+鏈表組成的,數組是 HashMap 的主體,鏈表則是主要爲了解決哈希衝突而存在的
  • TreeMap: 紅黑樹(自平衡的排序二叉樹)

如何選用集合?

主要根據集合的特色來選用,好比咱們須要根據鍵值獲取到元素值時就選用Map接口下的集合,須要排序時選擇TreeMap,不須要排序時就選擇HashMap,須要保證線程安全就選用ConcurrentHashMap.當咱們只須要存放元素值時,就選擇實現Collection接口的集合,須要保證元素惟一時選擇實現Set接口的集合好比TreeSet或HashSet,不須要就選擇實現List接口的好比ArrayList或LinkedList,而後再根據實現這些接口的集合的特色來選用。

開源項目推薦

做者的其餘開源項目推薦:

  1. JavaGuide:【Java學習+面試指南】 一份涵蓋大部分Java程序員所須要掌握的核心知識。
  2. springboot-guide : 適合新手入門以及有經驗的開發人員查閱的 Spring Boot 教程(業餘時間維護中,歡迎一塊兒維護)。
  3. programmer-advancement : 我以爲技術人員應該有的一些好習慣!
  4. spring-security-jwt-guide :從零入門 !Spring Security With JWT(含權限驗證)後端部分代碼。
相關文章
相關標籤/搜索