集合底層原理

Collection 單列集合

List 集合

List 集合的三個子類:

   ArrayList:底層是數組,查詢快(地址連續)、增刪慢、線程非安全。

   LinkedList:底層是鏈表,查詢慢、增刪快、無索引、線程非安全。

   Vector:底層是數組,線程安全。

一、ArrayList 原理

1.1 構造方法

ArrayList 底層是一個數組結構,當指定初始容量爲0時返回的是 EMPTY_ELEMENTDATA,不指定容量時返回 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

1.2 add 方法

1. 確認list容量,嘗試容量加1看有無必要,因爲當添加一個元素後,可能對超過集合初始容量,將會引發擴容。如果沒有超過初始容量則保存最小容量不變,無需對容量+1。

2. 添加元素

第一個方法得到數組的最小容量,避免資源浪費,第二個方法判斷最小容量是否大於數組長度,大於則調用grow()進行擴容。

grow()擴容的實現

newCapacity 這裏對數組容量擴容1.5倍,擴容完畢後調用 copyOf() ,完成對數組元素的拷貝。

總的來說 add(E e) 的實現: 首先檢查數組容量是否足夠,夠直接添加元素,不夠進行擴容。

1.3 get 方法

檢查角標是否越界,然後添加元素。

1.4 set 方法

檢查是否越界,替換元素。

1.5 remove() 方法

numMoved 是需要移動的元素個數,從 index+1位置開始拷貝 numMoved 個元素 到數組的 index 位置,就等於將被刪除元素後面的所有元素往前移動一個位置。因此List集合刪除效率低。

二、Vector 和 ArrayList 區別(面試)

Vector 是同步的(線程安全),其內部方法都使用 synchronized 修飾,同樣Vector因此效率比較低下。ArrayList 線程非安全。

ArrayList 底層擴容是在原有基礎上擴容1.5 倍,Vector是擴容 2 倍。

三、LinkedList 原理

LinkedList 集合特點:

1. 底層是雙向鏈表,查詢慢,增刪快。 2. 包含大量操作首尾的方法。

3.1 add 方法

瞭解過鏈表底層實現的應該對 LinkedList 的方法並不陌生,無論是添加還是刪除方法,都拆分爲 對頭部和尾部元素的具體操作。

這裏 add 方法採用尾插法,同樣也有 addFirst addLast 方法。

3.2 remove 方法

unlink(x) 完成對元素的刪除,刪除元素調用 equals 方法其實就是判斷元素是否存在鏈表中,unlink 方法中實現了雙向鏈表中刪除元素的操作。

將被刪除節點斷開,通過①②連接鏈表。

3.3 get 方法

判斷下標是否合法,然後調用node方法獲取鏈表元素。

3.4 set 方法

與 get 方法類似,根據下標判斷從頭遍歷還是從尾遍歷。

Set 集合

Set 集合的使用頻率相比 List 集合較低,通常結合 set 集合 元素不可重複的特點進行一些操作,比如尋找只出現一次的數字、或者在實際項目中需要存儲不可重複元素可以考慮使用 Set 集合。

一、Set集合常用子類

  • HashSet集合

    • A:底層數據結構是HashMap

    • B:允許null,非同步

  • TreeSet集合

    • A:底層數據結構是紅黑樹(是一個自平衡的二叉樹)

    • B:保證元素的排序方式,不允許null,非同步

  • LinkedHashSet集合

    • A:底層數據結構由HashMap和雙向鏈表組成。

    • B:允許null

二、HashSet集合

java.util.HashSet 集合 implements Set接口

HashSet是根據對象的哈希值來確定元素在集合中的存儲位置,因此具有良好的存取和查找性能。保證元素唯一性的方式依賴於:hashCode與equals方法。

2.1 HashSet特點:

    1.不允許存儲重複元素,允許元素爲null

    2.沒有索引,沒有帶索引的方法,也沒有for循環遍歷

    3.是一個無序的集合,存儲元素和取出元素的順序有可能不一致

    4.底層是HashMap(查詢速度非常快)

            Set<Integer> set = new HashSet<>(); //多態
            //使用add方法往集合中添加元素
            set.add(1);
            set.add(3);
            set.add(2);
            set.add(1);
            //使用迭代器遍歷set集合
            Iterator<Integer> it = set.iterator();
            while(it.hasNext()){
                Integer n = it.next();
                System.out.println(n); //1,2,3 不允許存儲重複元素,無序
            }
            //增強for遍歷set集合
            for(Integer n:set){
                System.out.print(n);
            }

2.2 HashSet集合存儲結構(哈希表)

在JDK1.8之前,哈希表底層採用數組+鏈表實現,即使用鏈表處理哈希衝突,同一hash值的鏈表都存儲在一個鏈表裏。但是當hash值相等的元素較多時,通過key值在鏈表中依次查找的效率較低。JDK1.8中,哈希表存儲採用數組+鏈表+紅黑樹實現,當鏈表長度超過(8)時,將鏈表轉換爲紅黑樹,這樣大大減少了查找時間。

要保證HashSet集合元素的唯一,就必須複寫hashCode和equals方法建立屬於當前對象的比較方式。(面試常考hashCode 和 equals 的聯繫)

① 重寫 hashCode 必須同時重寫equals。

② 相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。

③ 如果兩個對象的hashCode相同,它們並不一定相同。

Map 雙列集合

 java.util.Map<k,v>集合 存儲的元素是鍵值對類型,key - value是一一對應的映射關係。

Map集合特點:

key 和 value 數據類型是任意的,一般我們習慣使用key爲String類型,value爲Object類型。

key 不允許重複,value 可以重複,key只允許存在一個null的情況,value可以存在多個null值。

一、Collection 與 Map的區別(面試)

1. Map 集合存儲元素是鍵值對的方式,鍵唯一,值可以重複。

2. Collection 集合存儲元素是單獨出現的,Collection 的子接口Set值是唯一的,List是可重複的。

二、哈希表

List 集合底層是鏈表或者數組,存儲的順序和取出的順序是一致的,但同樣會帶來一個缺點,想要獲取某個元素,就要訪問所有元素,直到找到爲止。

在哈希表中,我們不在意元素的順序,能夠快速查找到元素。哈希表中每個對象按照哈希值保存在對應的位置上。哈希表是由數組+鏈表實現的,每個列表稱爲桶,

JDK1.8 之前 哈希值相同的對象以鏈表形式存儲在一個桶中,這種情況稱爲哈希衝突,此時需要用該對象與桶上的對象進行比較,看看是否已經存在桶中,如果存在就不添加。(這個過程使用hashCode和equals進行判斷,這也是之前我們說過爲什麼要同時重寫hashCode和equals的原因),接下來了解了HashMap的put方法底層原理,應該會對爲什麼同時重寫更加清晰了。

JDK1.8 之後 當鏈表長度超過8,會從鏈表轉換爲紅黑樹結構當哈希表存儲元素太多,需要對其進行擴展,創建一個桶更多的哈希表,那麼什麼時候需要進行擴容?裝填因子(load factor)決定了何時進行擴展,裝載因子默認爲0.75,表中如果超過75%的位置已經填入元素,將進行雙倍的擴展。

三、HashMap 原理

3.1 HashMap 類前註釋解讀

主要講了 HashMap 的一些特點:key value 允許爲null不保證有序不同步(線程非安全),想實現同步調用Collections.synchronizedMap,裝載因子爲0.75。使用迭代器進行遍歷。當知道要存儲的元素個數時,儘可能設置初始容量。

3.2 HashMap 屬性

當鏈表長度大於8,數組元素超過64 由鏈表結構轉化爲紅黑樹。

JDK1.7 時,在實例化之後,底層創建了長度是16的Entry數組。

JDK1.8 時,底層爲Node數組,調用put方法後創建長度爲16的數組。雖然長度爲16,但是切記,負載因子0.75,因此數組只存放12個元素。

3.3 put 方法

put 方法是HashMap的核心,也是面試常考點。

調用 hash(key) 方法,以key計算哈希值,hash 方法中得到key的hashCode 與 key的hashCode 高16位做異或運算,得到的值可以減少碰撞衝突的可能性。

1、hash(key),取key的hashcode進行高位運算,返回hash值
2、如果hash數組爲空,直接resize()
3、對hash進行取模運算計算,得到key-value在數組中的存儲位置i
(1)如果table[i] == null,直接插入Node<key,value>
(2)如果table[i] != null,判斷是否爲紅黑樹p instanceof TreeNode。
(3)如果是紅黑樹,則判斷TreeNode是否已存在,如果存在則直接返回oldnode並更新;不存在則直接插入紅黑樹,++size,超出threshold容量就擴容
(4)如果是鏈表,則判斷Node是否已存在,如果存在則直接返回oldnode並更新;不存在則直接插入鏈表尾部,判斷鏈表長度,如果大於8則轉爲紅黑樹存儲,++size,超出threshold容量就擴容

3.4 get 方法

 

我們知道HashMap 是通過key找value,獲取key的哈希值,調用getNode方法獲取對應value。

getNode 的實現比較簡單,先判斷計算出來的hashCode是否存在哈希表中,然後判斷是否在桶的首位,不在則在鏈表或紅黑樹中進行遍歷。

3.5 remove 方法

類似於put方法,這裏就不貼源碼,可以在util包下查看源碼。

計算key 的hash值,然後去找對應節點,找到後分三種情況進行刪除:1. 鏈表 2. 紅黑樹 3. 桶的首位

四、HashMap 與 Hashtable 對比

1、繼承的父類不同

Hashable繼承自Dictionary類,而HashMap繼承自AbstractMap類。但二者都實現了Map接口。

2、線程安全性不同

HashTable是線程安全的,HashTable方法都加入了Synchronized,HashMap是非安全的。

3、是否提供contains方法

HashMap把contains方法去掉了,改成containsValue和containsKey,因爲contains方法容易讓人引起誤解。

Hashtable則保留了contains,containsValue和containsKey三個方法,其中contains和containsValue功能相同。

4、key和value是否允許null值

其中key和value都是對象,並且不能包含重複key,但可以包含重複的value。

HashMap允許空值,鍵key最多隻能1個null值,value允許多個null,HashMap中不能由get()方法來判斷HashMap中是否存在某個鍵, 而應該用containsKey()方法來判斷。

Hashtable中,key和value都不允許出現null值。

5、兩個集合遍歷方式的內部實現上不同

Hashtable、HashMap都使用了 Iterator。而由於歷史原因,Hashtable還使用了Enumeration的方式

6、hash值不同

哈希值的使用不同,HashTable直接使用對象的hashCode。而HashMap重新計算hash值。

7、內部實現使用的數組初始化和擴容方式不同

HashTable在不指定容量的情況下的默認容量爲11,而HashMap爲16,Hashtable不要求底層數組的容量一定要爲2的整數次冪,而HashMap則要求一定爲2的整數次冪。

圖文並茂,對比很細,加粗部分可作爲面試主答點。

五、LinkedHashMap 原理

5.1 LinkedHashMap 類前註釋解讀

底層是哈希表+鏈表,允許爲null,線程不同步,插入元素有序

5.2 總結

這裏沒有對LinkedHashMap具體方法進行解析,因爲它繼承自HashMap,在很多操作上使用的是HashMap的方法。而HashMap的方法我們上面已經進行了分析。主要是個人對LinkedHashMap無感,平時也基本沒使用,大家有興趣可以看一下源碼。

六、TreeMap 原理

6.1 TreeMap 類前註釋解讀

底層是紅黑樹,實現NavigableMap 接口,可以根據key自然排序,也可以在構造方法上傳遞Camparator實現Map的排序。不同步

TreeMap 實現NavigableMap 接口,而NavigableMap接口繼承着SortedMap接口,致使我們的TreeMap是有序的。

key不能爲null。

6.2 構造方法

    //comparator 維護的變量爲null,使用自然排序
    public TreeMap() {
        comparator = null;
    }
    //設置一個維護變量傳入
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    //調用putAll將Map轉爲TreeMap
    public TreeMap(Map<? extends K, ? extends V> m) {
        comparator = null;
        putAll(m);
    }
    //使用buildFromSorted轉TreeMap
    public TreeMap(SortedMap<K, ? extends V> m) {
        comparator = m.comparator();
        try {
            buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
        } catch (java.io.IOException cannotHappen) {
        } catch (ClassNotFoundException cannotHappen) {
        }
    }

6.3 put 方法

6.4 get 方法

get 方法由getEntry進行具體實習

getEntry

當 comparator 不爲null時,getEntryUsingComparator(Object key) 調用Comparator 自己實現的方法獲取相應的值。

6.5 總結

TreeMap底層是紅黑樹,能夠實現該Map集合有序

如果在構造方法中傳遞了Comparator對象,那麼就會以Comparator對象的方法進行比較。否則,則使用Comparable的compareTo(T o)方法來比較。(自然排序)

  • 值得說明的是:如果使用的是compareTo(T o)方法來比較,key一定是不能爲null,並且得實現了Comparable接口的。

  • 即使是傳入了Comparator對象,不用compareTo(T o)方法來比較,key也是不能爲null的。

 七、ConcurrentHashMap

重頭戲來了~~~~

7.1 JDK1.7和JDK1.8 不同

1. JDK1.7 底層實現是:segments+HashEntry數組

ConcurrentHashMap使用分段鎖技術,Segment繼承了ReentrantLock,每個片段都有了一個鎖,叫做“鎖分段

我們知道Map中Hashtable是線程安全的,爲什麼要用ConCurrentHashMap?

Hashtable 是在每個方法上都加上了Synchronized 完成同步,效率極其低下,而ConcurrentHashMap通過在部分加鎖利用CAS算法來實現同步。

2. JDK 1.8底層實現是:哈希表+紅黑樹

檢索操作不用加鎖,get方法是非阻塞的,key和value都不允許爲空。

取消了1.7的 segments 分段鎖機制,採用CAS + volatile 樂觀鎖機制保證線程同步。

7.2 CAS算法和volatile簡單介紹

1. CAS(Compare and swap,比較與交換)

CAS 是一種基於樂觀鎖的操作。在java中鎖分爲樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個之前獲得鎖的線程釋放鎖之後,下一個線程纔可以訪問。而樂觀鎖採取了一種寬泛的態度,通過某種方式不加鎖來處理資源,比如通過給記錄加version來獲取數據,性能較悲觀鎖有很大的提高。

CAS有3個操作數

  • 內存值V

  • 舊的預期值A

  • 要修改的新值B

當且僅當預期值A和內存值V相同時,將內存值V修改爲B。CAS是通過無限循環來獲取數據的,若果在第一輪循環中,a線程獲取地址裏面的值被b線程修改了,那麼a線程需要自旋,到下次循環纔有可能機會執行。

2. volatile 關鍵字

volatile僅僅用來保證該變量對所有線程的可見性,但不保證原子性

可見性:在多線程環境下,當被volatile修飾的變量修改時,所有的線程都可以知道該變量被修改了

不保證原子性:修改變量(賦值)實質上是分爲好幾步操作,在這幾步操作中是不安全的。

7.2 put 方法

put操作採用CAS+synchronized 實現併發插入或更新操作。

  • 如果沒有初始化就先調用initTable()方法來進行初始化過程
  • 如果沒有hash衝突就直接CAS插入
  • 如果還在進行擴容操作就先進行擴容
  • 如果存在hash衝突,就加 synchronized 鎖來保證線程安全,這裏有兩種情況,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入,
  • 最後一個如果Hash衝突時會形成Node鏈表,在鏈表長度超過8,Node數組超過64時會將鏈表結構轉換爲紅黑樹的結構,break再一次進入循環
  • 如果添加成功就調用addCount()方法統計size,並且檢查是否需要擴容

 initTable 方法的實現

當要初始化時會通過CAS操作將sizeCtl置爲-1,而sizeCtl由volatile修飾,保證修改對後面線程可見。
這之後如果再有線程執行到此方法時檢測到sizeCtl爲負數,說明已經有線程在給擴容了,這個線程就會調用Thread.yield()讓出一次CPU執行時間。

7.3 get 方法

我們之前說到 get 操作不用加鎖,是非阻塞的,因爲 Node 節點被重寫了,設置了volatile關鍵字修飾,致使它每次獲取的都是最新設置的值。

八、CopyOnWriteArrayList 原理

CopyOnWriteArrayList是線程安全容器(相對於ArrayList),底層通過複製數組的方式來實現。

CopyOnWriteArrayList在遍歷的使用不會拋出ConcurrentModificationException異常,並且遍歷的時候就不用額外加鎖。

8.1 CopyOnWriteArrayList 基本結構

CopyOnWriteArrayList底層是數組,加鎖交由ReentrantLock來完成

    //可重入鎖對象
    final transient ReentrantLock lock = new ReentrantLock();

    //CopyOnWriteArrayList底層由數組實現,volatile修飾
    private transient volatile Object[] array;

    //得到數組
    final Object[] getArray() {
        return array;
    }
    //設置數組
    final void setArray(Object[] a) {
        array = a;
    }

    //初始化數組
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

8.2 add 方法

    public boolean add(E e) {
        //加鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //得到原數組長度和元素
            Object[] elements = getArray();
            int len = elements.length;
            //複製一個新數組
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //將元素添加到新數組
            newElements[len] = e;
            //將volatile Object[] array 的指向替換成新數組
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

在添加的時候上鎖,並複製一個新數組,添加操作在新數組上完成,將array指向到新數組中,最後解鎖。

8.3 set 方法

    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //得到原數組舊值
            Object[] elements = getArray();
            E oldValue = get(elements, index);
            //判斷新值和舊值是否相等
            if (oldValue != element) {
                int len = elements.length;
                //複製新數組,在新數組中完成修改
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                //array引用指向新數組
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
  • 在修改時,複製出一個新數組,修改的操作在新數組中完成,最後將新數組交由array變量指向

  • 寫加鎖,讀不加鎖,讀取數據是在原數組,因此保證了多線程下的數據一致性。

  • 類似於讀寫分離的思想,讀和寫分別在不同的容器完成,寫時進行讀操作並不會讀到髒數據。

8.4 CopyOnWriteArrayList缺點

佔用內存:如果CopyOnWriteArrayList經常要增刪改裏面的數據,經常要執行add()、set()、remove()進行新數組創建,那是比較耗費內存的。

數據一致性:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性

面試題總結

上面標註(面試)的這裏就不再闡述。。。

List 和 Map的區別

存儲結構不同:List 是存儲單列的集合,Map是存儲key-value鍵值對的集合

元素是否可重複:List 允許重複元素,Map不允許key重複

是否有序:List集合是有序的(存儲有序),Map集合是無序的。

Collection和Collections的區別

Collection是集合的上級接口,繼承它的有Set和List接口

Collections是集合的工具類,提供了一系列的靜態方法對集合的搜索、查找、同步等操作

ArrayList,Vector,LinkedList的存儲性能和特徵?

ArrayList和Vector都是使用數組方式存儲元素,可以直接按照索引查找元素、訪問快、增刪慢(相對的,極端情況不考慮)。

LinkedList使用雙向鏈表存儲數據、查找慢、增刪快(相對的,極端情況不考慮)。

Vector使用了synchronized方法,線程安全,性能上較ArrayList差。LinkedList也是線程不安全的,LinkedList提供了一些方法,使得它可以被當作堆棧或者隊列來使用。

Java中HashMap的key值要是爲類對象則該類需要滿足什麼條件?

需要同時重寫該類的hashCode()方法和equals方法。

原因:

插入元素時先計算hashCode值,相等則調用 equals() 方法,兩個key相同替換元素值,兩個key不相同,說明 hashCode 值是碰巧相同,則將新增元素放在桶裏。

我們一般認爲,只要兩個對象的成員變量的值是相等的,那麼我們就認爲這兩個對象是相等的,因此要重寫equals()f方法,重寫了equals(),就必須重寫hashCode()方法,因爲equals()認定了這兩個對象相同,而同一個對象調用hashCode()方法時,是應該返回相同的值的!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章