「Java面試題精華集」1w字的Java集合框架篇(2020最新版)附PDF版 !

本文已經收錄進個人 79K Star 的 Java 開源項目 JavaGuide: https://github.com/Snailclimb/JavaGuide (「Java學習+面試指南」一份涵蓋大部分Java程序員所須要掌握的核心知識。)相關閱讀: 完結撒花!Github接近80K點讚的Java面試指南來啦!

相關文章:html

  1. 「Java面試題精華集」1w字的Java集合框架篇(2020最新版)附PDF版 !

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

1.1. 集合概述

1.1.1. Java 集合概覽

從下圖能夠看出,在 Java 中除了以 Map 結尾的類以外, 其餘類都實現了 Collection 接口。java

而且,以 Map 結尾的類都實現了 Map 接口。git

image

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

  • List(對付順序的好幫手): 存儲的元素是有序的、可重複的。
  • Set(注重獨一無二的性質): 存儲的元素是無序的、不可重複的。
  • Map(用 Key 來搜索的專家): 使用鍵值對(kye-value)存儲,相似於數學上的函數 y=f(x),「x」表明 key,"y"表明 value,Key 是無序的、不可重複的,value 是無序的、可重複的,每一個鍵最多映射到一個值。

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

先來看一下 Collection 接口下面的集合。程序員

1.1.3.1. List

  • ArraylistObject[]數組
  • VectorObject[]數組
  • LinkedList: 雙向鏈表(JDK1.6 以前爲循環鏈表,JDK1.7 取消了循環)

1.1.3.2. Set

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

再來看看 Map 接口下面的集合。github

1.1.3.3. Map

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

1.1.4. 如何選用集合?

主要根據集合的特色來選用,好比咱們須要根據鍵值獲取到元素值時就選用 Map 接口下的集合,須要排序時選擇 TreeMap,不須要排序時就選擇 HashMap,須要保證線程安全就選用 ConcurrentHashMap面試

當咱們只須要存放元素值時,就選擇實現Collection 接口的集合,須要保證元素惟一時選擇實現 Set 接口的集合好比 TreeSetHashSet,不須要就選擇實現 List 接口的好比 ArrayListLinkedList,而後再根據實現這些接口的集合的特色來選用。算法

1.1.5. 爲何要使用集合?

當咱們須要保存一組類型相同的數據的時候,咱們應該是用一個容器來保存,這個容器就是數組,可是,使用數組存儲對象具備必定的弊端,
由於咱們在實際開發中,存儲的數據的類型是多種多樣的,因而,就出現了「集合」,集合一樣也是用來存儲多個數據的。shell

數組的缺點是一旦聲明以後,長度就不可變了;同時,聲明數組時的數據類型也決定了該數組存儲的數據的類型;並且,數組存儲的數據是有序的、可重複的,特色單一。
可是集合提升了數據存儲的靈活性,Java 集合不只能夠用來存儲不一樣類型不一樣數量的對象,還能夠保存具備映射關係的數據segmentfault

1.1.6. Iterator 迭代器

1.1.6.1. 迭代器 Iterator 是什麼?

public interface Iterator<E> {
    //集合中是否還有元素
    boolean hasNext();
    //得到集合中的下一個元素
    E next();
    ......
}

Iterator 對象稱爲迭代器(設計模式的一種),迭代器能夠對集合進行遍歷,但每個集合內部的數據結構多是不盡相同的,因此每個集合存和取都極可能是不同的,雖然咱們能夠人爲地在每個類中定義 hasNext()next() 方法,但這樣作會讓整個集合體系過於臃腫。因而就有了迭代器。設計模式

迭代器是將這樣的方法抽取出接口,而後在每一個類的內部,定義本身迭代方式,這樣作就規定了整個集合體系的遍歷方式都是 hasNext()next()方法,使用者不用管怎麼實現的,會用便可。迭代器的定義爲:提供一種方法訪問一個容器對象中各個元素,而又不須要暴露該對象的內部細節。

1.1.6.2. 迭代器 Iterator 有啥用?

Iterator 主要是用來遍歷集合用的,它的特色是更加安全,由於它能夠確保,在當前遍歷的集合元素被更改的時候,就會拋出 ConcurrentModificationException 異常。

1.1.6.3. 如何使用?

咱們經過使用迭代器來遍歷 HashMap,演示一下 迭代器 Iterator 的使用。

Map<Integer, String> map = new HashMap();
map.put(1, "Java");
map.put(2, "C++");
map.put(3, "PHP");
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
  Map.Entry<Integer, String> entry = iterator.next();
  System.out.println(entry.getKey() + entry.getValue());
}

1.1.7. 有哪些集合是線程不安全的?怎麼解決呢?

咱們經常使用的 Arraylist ,LinkedList,Hashmap,HashSet,TreeSet,TreeMapPriorityQueue 都不是線程安全的。解決辦法很簡單,可使用線程安全的集合來代替。

若是你要使用線程安全的集合的話, java.util.concurrent 包中提供了不少併發容器供你使用:

  1. ConcurrentHashMap: 能夠看做是線程安全的 HashMap
  2. CopyOnWriteArrayList:能夠看做是線程安全的 ArrayList,在讀多寫少的場合性能很是好,遠遠好於 Vector.
  3. ConcurrentLinkedQueue:高效的併發隊列,使用鏈表實現。能夠看作一個線程安全的 LinkedList,這是一個非阻塞隊列。
  4. BlockingQueue: 這是一個接口,JDK 內部經過鏈表、數組等方式實現了這個接口。表示阻塞隊列,很是適合用於做爲數據共享的通道。
  5. ConcurrentSkipListMap :跳錶的實現。這是一個Map,使用跳錶的數據結構進行快速查找。

1.2. Collection 子接口之 List

1.2.1. Arraylist 和 Vector 的區別?

  1. ArrayList 是 List 的主要實現類,底層使用 Object[ ]存儲,適用於頻繁的查找工做,線程不安全 ;
  2. Vector 是 List 的古老實現類,底層使用 Object[ ]存儲,線程安全的。

1.2.2. Arraylist 與 LinkedList 區別?

  1. 是否保證線程安全: ArrayListLinkedList 都是不一樣步的,也就是不保證線程安全;
  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)方法)。
  5. 內存空間佔用: ArrayList 的空 間浪費主要體如今在 list 列表的結尾會預留必定的容量空間,而 LinkedList 的空間花費則體如今它的每個元素都須要消耗比 ArrayList 更多的空間(由於要存放直接後繼和直接前驅以及數據)。

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

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

另外推薦一篇把雙向鏈表講清楚的文章: http://www.javashuo.com/article/p-fdbgdwfu-cy.html

雙向鏈表

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

雙向循環鏈表

1.2.2.2. 補充內容: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 接口才具備快速隨機訪問功能的!

1.2.3. 說一說 ArrayList 的擴容機制吧

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

1.3. Collection 子接口之 Set

1.3.1. comparable 和 Comparator 的區別

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

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

1.3.1.1. 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]

1.3.1.2. 重寫 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;
    }

    /**
     * T重寫compareTo方法實現按年齡來排序
     */
    @Override
    public int compareTo(Person o) {
        if (this.age > o.getAge()) {
            return 1;
        }
        if (this.age < o.getAge()) {
            return -1;
        }
        return 0;
    }
}
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-張三

1.3.2. 無序性和不可重複性的含義是什麼

一、什麼是無序性?無序性不等於隨機性 ,無序性是指存儲的數據在底層數組中並不是按照數組索引的順序添加 ,而是根據數據的哈希值決定的。

二、什麼是不可重複性?不可重複性是指添加的元素按照 equals()判斷時 ,返回 false,須要同時重寫 equals()方法和 HashCode()方法。

1.3.3. 比較 HashSet、LinkedHashSet 和 TreeSet 三者的異同

HashSet 是 Set 接口的主要實現類 ,HashSet 的底層是 HashMap,線程不安全的,能夠存儲 null 值;

LinkedHashSet 是 HashSet 的子類,可以按照添加的順序遍歷;

TreeSet 底層使用紅黑樹,可以按照添加元素的順序進行遍歷,排序的方式有天然排序和定製排序。

1.4. Map 接口

1.4.1. HashMap 和 Hashtable 的區別

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

1.4.2. 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()方法用來判斷對象的相等性,

1.4.3. HashMap 和 TreeMap 區別

TreeMapHashMap 都繼承自AbstractMap ,可是須要注意的是TreeMap它還實現了NavigableMap接口和SortedMap 接口。

image

實現 NavigableMap 接口讓 TreeMap 有了對集合內元素的搜索的能力。

實現SortMap接口讓 TreeMap 有了對集合中的元素根據鍵排序的能力。默認是按 key 的升序排序,不過咱們也能夠指定排序的比較器。示例代碼以下:

/**
 * @author shuang.kou
 * @createTime 2020年06月15日 17:02:00
 */
public class Person {
    private Integer age;

    public Person(Integer age) {
        this.age = age;
    }

    public Integer getAge() {
        return age;
    }


    public static void main(String[] args) {
        TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
            @Override
            public int compare(Person person1, Person person2) {
                int num = person1.getAge() - person2.getAge();
                return Integer.compare(num, 0);
            }
        });
        treeMap.put(new Person(3), "person1");
        treeMap.put(new Person(18), "person2");
        treeMap.put(new Person(35), "person3");
        treeMap.put(new Person(16), "person4");
        treeMap.entrySet().stream().forEach(personStringEntry -> {
            System.out.println(personStringEntry.getValue());
        });
    }
}

輸出:

person1
person4
person2
person3

能夠看出,TreeMap 中的元素已是按照 Person 的 age 字段的升序來排列了。

上面,咱們是經過傳入匿名內部類的方式實現的,你能夠將代碼替換成 Lambda 表達式實現的方式:

TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
  int num = person1.getAge() - person2.getAge();
  return Integer.compare(num, 0);
});

綜上,相比於HashMap來講 TreeMap 主要多了對集合中的元素根據鍵排序的能力以及對集合內元素的搜索的能力。

1.4.4. 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 的區別

對於基本類型來講,== 比較的是值是否相等;

對於引用類型來講,== 比較的是兩個引用是否指向同一個對象地址(二者在內存中存放的地址(堆內存地址)是否指向同一個地方);

對於引用類型(包括包裝類型)來講,equals 若是沒有被重寫,對比它們的地址是否相等;若是 equals()方法被重寫(例如 String),則比較的是地址裏的內容。

1.4.5. HashMap 的底層實現

1.4.5.1. 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

1.4.5.2. JDK1.8 以後

相比於以前的版本, JDK1.8 以後在解決哈希衝突時有了較大的變化,當鏈表長度大於閾值(默認爲 8)(將鏈表轉換成紅黑樹前會判斷,若是當前數組的長度小於 64,那麼會選擇先進行數組擴容,而不是轉換爲紅黑樹)時,將鏈表轉化爲紅黑樹,以減小搜索時間。

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

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

1.4.6. 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 的冪次方。

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

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

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

1.4.8. HashMap 有哪幾種常見的遍歷方式?

HashMap 的 7 種遍歷方式與性能分析!

1.4.9. 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,競爭會愈來愈激烈效率越低。

二者的對比圖:

HashTable:

HashTable全表鎖

<p style="text-align:right;font-size:13px;color:gray">http://www.cnblogs.com/chengx...;</p>

JDK1.7 的 ConcurrentHashMap:

JDK1.7的ConcurrentHashMap

<p style="text-align:right;font-size:13px;color:gray">http://www.cnblogs.com/chengx...;</p>

JDK1.8 的 ConcurrentHashMap:

JDK1.8 的 ConcurrentHashMap

JDK1.8 的 ConcurrentHashMap 不在是 Segment 數組 + HashEntry 數組 + 鏈表,而是 Node 數組 + 鏈表 / 紅黑樹。不過,Node 只能用於鏈表的狀況,紅黑樹的狀況須要使用 TreeNode。當衝突鏈表達到必定長度時,鏈表會轉換成紅黑樹。

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

1.4.10.1. 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 的鎖。

1.4.10.2. JDK1.8 (上面有示意圖)

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

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

1.5. Collections 工具類

Collections 工具類經常使用方法:

  1. 排序
  2. 查找,替換操做
  3. 同步控制(不推薦,須要線程安全的集合類型時請考慮使用 JUC 包下的併發集合)

1.5.1. 排序操做

void reverse(List list)//反轉
void shuffle(List list)//隨機排序
void sort(List list)//按天然排序的升序排序
void sort(List list, Comparator c)//定製排序,由Comparator控制排序邏輯
void swap(List list, int i , int j)//交換兩個索引位置的元素
void rotate(List list, int distance)//旋轉。當distance爲正數時,將list後distance個元素總體移到前面。當distance爲負數時,將 list的前distance個元素總體移到後面

1.5.2. 查找,替換操做

int binarySearch(List list, Object key)//對List進行二分查找,返回索引,注意List必須是有序的
int max(Collection coll)//根據元素的天然順序,返回最大的元素。 類比int min(Collection coll)
int max(Collection coll, Comparator c)//根據定製排序,返回最大元素,排序規則由Comparatator類控制。類比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的全部元素。
int frequency(Collection c, Object o)//統計元素出現次數
int indexOfSubList(List list, List target)//統計target在list中第一次出現的索引,找不到則返回-1,類比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替換舊元素

1.5.3. 同步控制

Collections 提供了多個synchronizedXxx()方法·,該方法能夠將指定集合包裝成線程同步的集合,從而解決多線程併發訪問集合時的線程安全問題。

咱們知道 HashSetTreeSetArrayList,LinkedList,HashMap,TreeMap 都是線程不安全的。Collections 提供了多個靜態方法能夠把他們包裝成線程同步的集合。

最好不要用下面這些方法,效率很是低,須要線程安全的集合類型時請考慮使用 JUC 包下的併發集合。

方法以下:

synchronizedCollection(Collection<T>  c) //返回指定 collection 支持的同步(線程安全的)collection。
synchronizedList(List<T> list)//返回指定列表支持的同步(線程安全的)List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步(線程安全的)Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步(線程安全的)set。

1.6. 其餘重要問題

1.6.1. 什麼是快速失敗(fail-fast)?

快速失敗(fail-fast) 是 Java 集合的一種錯誤檢測機制。在使用迭代器對集合進行遍歷的時候,咱們在多線程下操做非安全失敗(fail-safe)的集合類可能就會觸發 fail-fast 機制,致使拋出 ConcurrentModificationException 異常。 另外,在單線程下,若是在遍歷過程當中對集合對象的內容進行了修改的話也會觸發 fail-fast 機制。

注:加強 for 循環也是藉助迭代器進行遍歷。

舉個例子:多線程下,若是線程 1 正在對集合進行遍歷,此時線程 2 對集合進行修改(增長、刪除、修改),或者線程 1 在遍歷過程當中對集合進行修改,都會致使線程 1 拋出 ConcurrentModificationException 異常。

爲何呢?

每當迭代器使用 hashNext()/next()遍歷下一個元素以前,都會檢測 modCount 變量是否爲 expectedModCount 值,是的話就返回遍歷;不然拋出異常,終止遍歷。

若是咱們在集合被遍歷期間對其進行修改的話,就會改變 modCount 的值,進而致使 modCount != expectedModCount ,進而拋出 ConcurrentModificationException 異常。

注:經過 Iterator 的方法修改集合的話會修改到 expectedModCount 的值,因此不會拋出異常。
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

好吧!相信你們已經搞懂了快速失敗(fail-fast)機制以及它的原理。

咱們再來趁熱打鐵,看一個阿里巴巴手冊相關的規定:

有了前面講的基礎,咱們應該知道:使用 Iterator 提供的 remove 方法,能夠修改到 expectedModCount 的值。因此,纔不會再拋出ConcurrentModificationException 異常。

1.6.2. 什麼是安全失敗(fail-safe)呢?

明白了快速失敗(fail-fast)以後,安全失敗(fail-safe)咱們就很好理解了。

採用安全失敗機制的集合容器,在遍歷時不是直接在集合內容上訪問的,而是先複製原有集合內容,在拷貝的集合上進行遍歷。因此,在遍歷過程當中對原集合所做的修改並不能被迭代器檢測到,故不會拋 ConcurrentModificationException 異常。

1.6.3. Arrays.asList()避坑指南

最近使用Arrays.asList()遇到了一些坑,而後在網上看到這篇文章:Java Array to List Examples 感受挺不錯的,可是還不是特別全面。因此,本身對於這塊小知識點進行了簡單的總結。

1.6.3.1. 簡介

Arrays.asList()在平時開發中仍是比較常見的,咱們可使用它將一個數組轉換爲一個 List 集合。

String[] myArray = { "Apple", "Banana", "Orange" };
List<String> myList = Arrays.asList(myArray);
//上面兩個語句等價於下面一條語句
List<String> myList = Arrays.asList("Apple","Banana", "Orange");

JDK 源碼對於這個方法的說明:

/**
 *返回由指定數組支持的固定大小的列表。此方法做爲基於數組和基於集合的API之間的橋樑,與           Collection.toArray()結合使用。返回的List是可序列化並實現RandomAccess接口。
 */
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

1.6.3.2. 《阿里巴巴 Java 開發手冊》對其的描述

Arrays.asList()將數組轉換爲集合後,底層其實仍是數組,《阿里巴巴 Java 開發手冊》對於這個方法有以下描述:

阿里巴巴Java開發手-Arrays.asList()方法

1.6.3.3. 使用時的注意事項總結

傳遞的數組必須是對象數組,而不是基本類型。

Arrays.asList()是泛型方法,傳入的對象必須是對象數組。

int[] myArray = { 1, 2, 3 };
List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1
System.out.println(myList.get(0));//數組地址值
System.out.println(myList.get(1));//報錯:ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0);
System.out.println(array[0]);//1

當傳入一個原生數據類型數組時,Arrays.asList() 的真正獲得的參數就不是數組中的元素,而是數組對象自己!此時 List 的惟一元素就是這個數組,這也就解釋了上面的代碼。

咱們使用包裝類型數組就能夠解決這個問題。

Integer[] myArray = { 1, 2, 3 };

使用集合的修改方法:add()remove()clear()會拋出異常。

List myList = Arrays.asList(1, 2, 3);
myList.add(4);//運行時報錯:UnsupportedOperationException
myList.remove(1);//運行時報錯:UnsupportedOperationException
myList.clear();//運行時報錯:UnsupportedOperationException

Arrays.asList() 方法返回的並非 java.util.ArrayList ,而是 java.util.Arrays 的一個內部類,這個內部類並無實現集合的修改方法或者說並無重寫這些方法。

List myList = Arrays.asList(1, 2, 3);
System.out.println(myList.getClass());//class java.util.Arrays$ArrayList

下圖是java.util.Arrays$ArrayList的簡易源碼,咱們能夠看到這個類重寫的方法有哪些。

private static class ArrayList<E> extends AbstractList<E>
        implements RandomAccess, java.io.Serializable
    {
        ...

        @Override
        public E get(int index) {
          ...
        }

        @Override
        public E set(int index, E element) {
          ...
        }

        @Override
        public int indexOf(Object o) {
          ...
        }

        @Override
        public boolean contains(Object o) {
           ...
        }

        @Override
        public void forEach(Consumer<? super E> action) {
          ...
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
          ...
        }

        @Override
        public void sort(Comparator<? super E> c) {
          ...
        }
    }

咱們再看一下java.util.AbstractListremove()方法,這樣咱們就明白爲啥會拋出UnsupportedOperationException

public E remove(int index) {
    throw new UnsupportedOperationException();
}

最後

做者介紹: Github 80k Star 項目 JavaGuide(公衆號同名) 做者。每週都會在公衆號更新一些本身原創乾貨。 Java 程序員面試必備的《Java面試突擊》V3.0 PDF 版本掃碼關注下面的公衆號,在後臺回覆 "面試突擊" 便可免費領取!

個人公衆號

相關文章
相關標籤/搜索