全面解析Java常用容器(從底層結構解析HashMap、ConcurrentHashMap、ArrayList、Vector、LinkedList等常用容器之間的區別和特點)

前言

本篇博文主要是通過解析HashMap、ConcurrentHashMap、TreeMap、Hashtable、LinkedHashMap、HashSet、TreeSet 、ArrayList、Vector、CopyOnWriteArrayList、LinkedList、BitSet、Pair等常用容器的底層結構來分析各種容器的特點、用途以及它們之間的聯繫和區別。

對於文章中出現的Java鎖相關知識不是很清楚的可以翻閱我的另外兩篇博文《Java高效併發之synchronized關鍵字和鎖優化》《Volatile關鍵字》

總覽

Java容器庫可以分爲兩大類,一類是繼承自Collection接口,繼承自Collection接口的這些容器類的元素是單個元素對象,容器中元素對象按照特定規則排列而成獨立序列;另外一類是繼承自Map接口,繼承自Map接口的容器類的元素是鍵值對。

抽象類是對類的抽象,是一種模板的定義;而接口是對行爲的抽象,是一個行爲規範,這裏通過頂層接口Collection和Map爲下層接口以及子類容器定義了容器必須對外提供的行爲方法,當然Set、List、Queue等接口也會在繼承父類接口的基礎上添加屬於自己這類容器所特有的行爲,比如List接口就定義了通過索引位置獲得對象的方法get(int index)。

容器與數組最大一個區別是數組在初始化時需要指定大小,而容器都是不需要,有着自己的默認初始的大小和自己的擴容機制。
在這裏插入圖片描述
在這裏插入圖片描述

Map

Map類容器是存儲鍵值對的容器,除了迭代獲取元素值還可以通過鍵值獲得其元素值。鍵(Key)的值唯一不可重複;不同的鍵值可以擁有相同的元素值。

面試中面試官最常問的問題就是HashMap和ConcurrentHashMap的區別。我們都知道的是HashMap不是線程安全的而ConcurrentHashMap是線程安全的,但爲什麼會是這樣嘞?這裏需要通過分析他們的底層結構以及操作實現的機制來進行說明。當然Map的子類容器常用的還有TreeMap和LinkedHashMap以及被棄用的HashTable等,這裏通過分析他們的底層結構來進行他們之間的聯繫和區別的比較。

HashMap(線程不安全)

在介紹HashMap的特點之前,先說明下HashMap的底層結構:
JDK1.8之前: 數組(HashMap.Node<K, V>[] table)+鏈表(HashMap.Node<K, V> next )
JDK1.8: 數組(HashMap.Node<K, V>[] table)+鏈表(HashMap.Node<K, V> next)+紅黑樹(TreeNode<K, V> )(當鏈表中結點數大於閾值(默認爲8)且數組大小大於64時,鏈表就變成紅黑樹)
在這裏插入圖片描述

通過插入和查找兩個過程來說明HashMap的實現原理。

HashMap底層實現原理

HashMap基於哈希表實現,HashMap的數組就是hash桶,而鏈表和紅黑樹是爲了解決hash衝突而加入的數據結構(拉鍊法解決hash衝突)。之所以加入紅黑樹是因爲當鏈表過長時查找鍵值對遍歷鏈表效率太低,因此JDK1.8中加入紅黑樹(自平衡的二叉查找樹)。當數組的大小大於64且數組中的鏈表長度大於閾值(默認爲8),鏈表轉化成紅黑樹,增加查找效率;當數組的大小小於64不管鏈表長度多長,鏈表都不會化成紅黑樹。

當我們使用put(K,V)來進行元素插入時,原理步驟如下:
(1)獲得元素的位置:先通過使用hash函數對鍵值對象的hash碼進行運算得到hash值,通過hash值%數組大小(n)來獲得該鍵值對映射在數組中的位置table[i]。(數組的大小一定因此可能多個鍵值對映射到同一個位置產生衝突)
(2)判斷該位置table[i]上是否已有Node結點,若無則直接創建新Node結點(新結點內值爲新鍵值對),將新結點放入該數組位置;若有則判斷該數組結點是否是紅黑樹的結點,若是則說明鏈表已轉換爲紅黑樹(按照hash值排序),自根結點從上往下遍歷紅黑樹進行查找,進行hash值和Key值比較,若有相同Key值的結點則返回該結點,跳到步驟(4);若無相同Key值,則找到合適的位置,插入新new的TreeNode結點。
(3)若不是紅黑樹則說明用於解決哈希衝突的還是鏈表,遍歷鏈表,通過equals(Key)比較要插入的鍵值對的鍵值已存在,若存在返回已存在結點,跳到步驟(4);若不存在則new一個新的Node結點插入到鏈表尾部,並判斷該鏈表是否需要變成紅黑樹。
(4)鏈表或者紅黑樹中已存在需要插入的鍵值對的鍵值,則用新值替代舊值。
(5)判斷容器中的元素是否超過閾值,是否需要擴容。

插入元素源碼分析如下:

   //利用hash函數對hashcode進行再次運算,使得hash值更加均勻分佈
   static final int hash(Object key) {
        int h;
        return key == null ? 0 : (h = key.hashCode()) ^ h >>> 16;
    }
    //插入元素函數,調用putVal()進行元素插入
   public V put(K key, V value) {
        return this.putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        HashMap.Node[] tab;
        int n;
        //使用懶加載機制,第一次進行put時才真正賦予默認大小的內存。
        if ((tab = this.table) == null || (n = tab.length) == 0) {
            n = (tab = this.resize()).length;
        }

        Object p;
        int i;
        //映射的數組位置是否已有結點,若無則創建新Node插入該位置
        if ((p = tab[i = n - 1 & hash]) == null) {
            tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
        } else {    //若有結點
            Object e;
            Object k;
      //該數組的結點是否key值與待插入的相同,相同則賦值用以後面的新值替代舊值
            if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
                e = p;
            } 
            //若key值不相同,則判斷結點是否是紅黑樹的結點,若是則將結點插入紅黑樹中
            else if (p instanceof HashMap.TreeNode) {
                e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
            }
            //若是鏈表,則遍歷鏈表是否有相同Key的結點,若有則返回結點,用以後面新值替代舊值,若無則將值插入到鏈表尾部
             else {
                int binCount = 0;
                while(true) {
                   //達到尾部,將新結點插入尾部
                    if ((e = ((HashMap.Node)p).next) == null) {
                        ((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
                        //當鏈表長度等於8時,鏈表變爲紅黑樹
                        if (binCount >= 7) {
                            this.treeifyBin(tab, hash);
                        }
                        break;
                    }
                     //當發現鏈表中具有相同Key值的結點,停止遍歷,返回結點  
                    if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
                        break;
                    }
                   
                    p = e;
                    ++binCount;
                }
            }
            //(鏈表或者紅黑樹中已存在該key值時)將返回的結點,進行新值替代舊值 
            if (e != null) {
                V oldValue = ((HashMap.Node)e).value;
                if (!onlyIfAbsent || oldValue == null) {
                    ((HashMap.Node)e).value = value;
                }

                this.afterNodeAccess((HashMap.Node)e);
                return oldValue;
            }
        }
        ++this.modCount;
         //若插入新結點,則將HashMap容器中的元素加1,並判斷是否查過容量閾值,若超過則擴容
        if (++this.size > this.threshold) {
            this.resize();
        }

        this.afterNodeInsertion(evict);
        return null;
    }

當我們使用get(K)來進行元素查找時,原理步驟如下:
(1)通過用hash函數對鍵值key的hashcode進行運算獲得hash值,再通過hash值%數組大小(n)得到該鍵值對所在的數組位置;
(2)判斷數組上的第一個結點的鍵值是否就是待查找的key值,若是,返回第一個結點;
(3)若不是則判斷數組上的第一個結點是否是紅黑樹的結點,若是則遍歷紅黑樹進行查找,返回結點的元素值(value);
(4)若不是紅黑樹結點,則遍歷鏈表進行查找。

查找元素源碼分析如下:

  //通過調用getNode()獲得待找的結點,若結點爲null返回null否則返回結點的元素值(value)
 public V get(Object key) {
        HashMap.Node e;
        return (e = this.getNode(hash(key), key)) == null ? null : e.value;
    }

    final HashMap.Node<K, V> getNode(int hash, Object key) {
        HashMap.Node[] tab;
        HashMap.Node first;
        int n;
        //當數組該位置不爲空,進行查找,否則返回null
        if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) {
            Object k;
            //判斷第一個結點的key值和hash值是否與待找的相同,相同則返回第一個結點,否則繼續往下進行
            if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) {
                return first;
            }

            HashMap.Node e;
            //判斷第一個結點是否是紅黑樹結點,若是則取紅黑樹中進行查找
            if ((e = first.next) != null) {
                if (first instanceof HashMap.TreeNode) {
                    return ((HashMap.TreeNode)first).getTreeNode(hash, key);
                }
            //若不是則遍歷鏈表進行查找,找到則返回該結點
                do {
                    if (e.hash == hash && ((k = e.key) == key || key != null && key.equals(k))) {
                        return e;
                    }
                } while((e = e.next) != null);
            }
        }

        return null;
    }

HashMap的特徵

1、HashMap是線程不安全的,通過以上在容器中插入元素的代碼,你可以發現在插入元素的整個過程都沒有使用到鎖機制,其實不止是插入操作,其它任何改變或者讀取元素值的操作都沒加鎖,因此在多線程中會出現擴容死循環以及讀寫值錯誤。
2、HashSet和LinkedHashMap都是基於HashMap的基礎上實現的;HashMap與ConcurrentHashMap底層結構相同,主要差別是ConcurrentHashMap在操作過程中使用了鎖機制,因此它是線程安全的,而HashMap不是。
3、鍵值允許爲NULL,但因爲唯一性,所以鍵值爲NULL的鍵值對只有一個。
4、HashMap的擴容機制是當容器中的元素數量大於等initialCapacity(數組長度)*loadFactor(加載因子,默認爲0.75)的大小時發生擴容,擴充爲原來的兩倍,對容器中的元素重新進行映射,映射到新的位置。HashMap 默認的初始化大小爲16。之後每次擴充,容量變爲原來的2倍。之所以必須是兩倍的原因是HashMap的大小必須是2的倍數,因爲HashMap源碼在進行元素位置定位時爲了提高效率,使用的是 n - 1 & hash而不是hash&%(n-1),雖然它們的含義都是通過用hash值對數組的大小n進行取餘來進行映射獲得鍵值對的位置,但hash&%(n-1) 的計算更快,但它要求數組的大小必須是2的倍數。

ConcurentHashMap(線程安全)

底層結構:
JDK1.8之前:分段的數組(Segment[],一個 Segment對象包含一個 HashEntry 數組)+鏈表(每個 HashEntry 是一個鏈表結構的元素)
在這裏插入圖片描述
JDK1.8:數組+鏈表+紅黑樹(數組的每一個位置table[i]上都有一把鎖)
在這裏插入圖片描述

ConcurrentHashMap與HashMap的區別就在於ConcurrentHashMap是線程安全的,而它的線程安全的實現是因爲在操作過程中使用鎖機制。在JDK1.8之前,底層利用分段鎖的思想,使用了CAS自旋鎖和+ReentrantLock(可重入鎖)來保證ConcurrentHashMap的插入元素、修改元素、獲得元素值、擴容等操作在多線程中不會出錯。JDK1.8之後,ConcurrentHashMap的底層結構變成了數組+鏈表+紅黑樹,使用的鎖機制也變成了CAS自旋鎖Synchronized關鍵字。JDK1.7中鎖是加在Segment對象上(1個segment對象可能包含多個HashEntry 對象),而JDK1.8中鎖是加在數組的第一個Node結點(相當於JDK1.7的HashEntry 對象)上,鎖的粒度較之前更小,併發度提高了。而且通過加入紅黑樹,減少了遍歷查找的消耗。

JDK1.8之前的插入過程:當調⽤ConcurrentHashMap的put⽅法時,(1)先根據key計算出鍵值對在對應的Segment[]的數組中的位置segments[j],確定好當前鍵值對應該插⼊到數組中哪個Segment對象中,(2)如果segments[j]爲空,則使用自旋鎖CAS的⽅式在j位置⽣成⼀個 Segment對象。(3)然後調用Segment對象的put⽅法。 Segment對象的put方法會先對Segment對象加鎖,然後也根據key計算出對應的HashEntry[]的數組下標i,然後將 key,value封裝爲HashEntry對象插入或者替換進該位置的鏈表中,此過程和JDK7的HashMap的put⽅法⼀樣,然後解鎖。

JDK1.8的插入過程:(1)首先根據key計算對應的數組下標i,如果該位置沒有元素,則通過⾃旋鎖CAS的⽅法去向該位置賦值。( 2).如果該位置有元素,則synchronized會加鎖 (3)加鎖成功之後,在判斷該元素的類型 a. 如果是鏈表節點則進⾏添加節點到鏈表中 b. 如果是紅⿊樹則添加節點到紅⿊樹 (4. )添加成功後,判斷是否需要進⾏樹化 (5). 插入新結點後,ConcurrentHashMap的元素個數加1,但是這個操作也是需要併發安全 的,並且元素個數加1成功後,會繼續判斷是否要進⾏擴容,如果需要,則會進⾏擴容,擴容過程也需要加鎖。

HashTable(已棄用,線程安全)

底層結構:數組+鏈表
在這裏插入圖片描述

它與HashMap的重要區別是它是線程安全的,它使用synchronized關鍵字對整個數組進行加鎖(相當於全表鎖,對整個容器上了鎖),因此它的併發程度就很低,效率極其低下。這可能就是它被棄用的重要原因之一,非線程安全有HashMap,線程安全有ConcurrentHashMap,都比它好使。它不允許鍵值爲NULL,插入鍵值NULL的鍵值對會拋出異常,而HashMap允許。Hashtable 默認的初始大小爲11,之後每次擴充,容量變爲原來的2n+1。

LinkedHashMap(線程不安全)

底層結構: 數組+鏈表+紅黑樹+雙向鏈表
在這裏插入圖片描述

LinkedHashMap的底層結構與HashMap幾乎一致,區別在於LinkedHashMap在HashMap的結構基礎之上,在各個結點之間維護了一條雙向鏈表。HashMap無序,而LinkedHashMap有序,而這一點是通過它維護的雙向鏈表實現的。LinkedHashMap繼承自HashMap,很多方法都直接調用的父類HashMap,僅爲維護雙向鏈表重寫了部分方法。它在 HashMap 基礎上,通過維護一條雙向鏈表,添加了可以通過按插入順序或者訪問順序進行排序的功能。因此我們在遍歷容器的元素時是按照插入順序或者訪問順序來進行遍歷的,按訪問順序來進行遍歷的特點可以使得LinkedHashMap用來做支持LRU算法的緩存。

默認情況下,LinkedHashMap 是按插入順序維護鏈表,雙向鏈表的插入順序是在元素插入容器的過程中進行維護的;不過我們可以在初始LinkedHashMap時,指定 accessOrder 參數爲 true,即可讓它按訪問順序維護雙向鏈表。當我們調用get/getOrDefault/replace等方法進行訪問元素時,只需要將這些方法訪問的節點移動到鏈表的尾部即可。

具體過程原理我就不詳述的,如果感興趣的可以自行搜索或者查看我看過的一篇博客(上面的圖就是偷他的,寫的還不錯)《LinkedHashMap 源碼詳細分析(JDK1.8)》

TreeMap(線程不安全)

底層結構: 紅黑樹

TreeMap的底層結構只是紅黑樹,紅黑樹是自平衡的二叉查找樹,因此TreeMap可使鍵值對按照鍵值大小進行排序(升序),當然也可以通過自定義排序規則(實現Comparator接口,重寫compare()方法)。TreeMap 實現了NavigableMap接口,它支持一系列的導航方法。比如返回有序的key集合。TreeMap 實現了Cloneable接口,它能被克隆。TreeMap 實現了java.io.Serializable接口,意味着它支持序列化。

TreeMap的性能雖然比HashMap低一些,但它支持有序查找。若無排序要求,推薦使用HashMap,若有排序要求則使用TreeMap。注意TreeMap和LinkedHashMap的區別,LinkedHashMap是按照插入順序或者訪問順序進行排序,而TreeMap是按照key值的大小或者自定義進行排序。

Map容器小結

Map的應用場景是鍵值對,通過索引找到對象。單線程中,需要使用Map容器時,優先使用HashMap;當有按照鍵值大小進行排序的需求時,選用TreeMap;當有按照插入順序或者訪問順序進行排序的需求時,選用LinkedHashMap;多線程中,選用ConcurrentHashMap。

Set

Set容器接口繼承自Collection接口,容器內存儲的是對象而不是鍵值對,容器內的元素是無序的且是不可重複的。

HashSet(線程不安全)

底層結構: 數組(HashMap.Node<K, V>[] table)+鏈表+紅黑樹

HashSet是在HashMap的基礎上實現的,底層結構與HashMap相同,HashSet除了幾個不得不自己實現的方法,例如clone()方法,其它方法都是直接調用了HashMap的方法,因此HashSet的插入、刪除、擴容等機制基本跟HashMap一樣。HashSet使用HashMap鍵值對中的key存儲其元素對象,HashSet的重要特徵容器內的元素都是不可重複的利用的就是HashMap的鍵值不可重複的特點。

HashSet的添加元素代碼如以下所示:

private transient HashMap<E, Object> map;
//HashSet添加元素的方法,直接調用了HashMap的插入元素的方法
public boolean add(E e) {
        //將待插入的值插入到HashMap的key中,而HashMap的value值爲一個類常量Object對象
        return this.map.put(e, PRESENT) == null;
    }

TreeSet(線程不安全)

底層結構: 紅黑樹

與HashSet類似,TreeSet基於TreeMap實現,底層結構與TreeMap相同,TreeSet除了幾個不得不自己實現的方法,例如clone()方法,其它方法都是直接調用了TreeMap的方法,因此TreeSet的插入、刪除、擴容等機制基本跟TreeMap一樣。TreeSet使用TreeMap鍵值對中的key存儲其元素對象,TreeSet的重要特徵容器內的元素都是不可重複的利用的就是TreeMap的鍵值不可重複的特點。TreeSet擁有與TreeMap一樣的特點可通過鍵值進行排序(默認按鍵值升序排列,但可自定義排序規則),因此TreeSet是一個元素有序的Set容器。

Set容器小結

Set的特徵就是元素的不可重複性,應用場景也是要求元素值獨一無二。若無有序要求,在要求元素不可重複的場景中,推薦使用HashSet,它底層是哈希表,查找時間複雜度爲O(1);TreeSet的底層是紅黑樹,查找時間複雜度爲O(logN),整體HashSet性能更好。若有元素有序要求,則使用TreeSet。

List

List類容器的最大特點是容器內元素可重複;用戶可以精確控制列表中每個元素的插入位置,也可通過索引訪問元素,是一個有序的容器。

ArrayList(線程不安全)

底層結構: 動態數組( Object[] elementData)

ArrayList應該是我們使用最多的容器了,經常在初始化不知道該分配多大空間時用它來替代數組。它是基於動態數組實現的,也就是說它的數據結構是順序結構,順序結構的特點是存儲密度大隨機查找效率高(O(1)),隨機插入/刪除效率低(O(n)),因此它適合應用的場景是經常做隨機查找、很少做插入和刪除操作。ArrayList 採用數組存儲,所以插入和刪除元素的時間複雜度受元素位置的影響。 比如:執行add(E e)方法的時候, ArrayList 會默認在將指定的元素追加到此列表的末尾,這種情況時間複雜度就是O(1)。但是如果要在指定位置 i 插入和刪除元素的話(add(int index, E element))時間複雜度就爲 O(n-i)。因爲在進行上述操作的時候集合中第 i 和第 i 個元素之後的(n-i)個元素都要執行向後位/向前移一位的操作。

ArrayList的擴容機制-ArrayList在初始化時只分配了一個空數組,沒有分配容量,當第一次插入元素時,ArrayList的底層數組擴容爲10,後來每次數組內的元素達到當前分配的最大容量時,進行擴容,擴容爲當前分配容量的1.5倍。

這裏就不進行源碼分析了,如果感興趣可以翻閱下《ArrayList源碼分析》這篇文章,裏面有清楚的源碼分析。

Vector(線程安全)

底層結構: 動態數組

與ArrayList相同的是,它的底層結構也是動態數組,適合用於經常做隨機查找的場景。不同的是Vector容器是線程安全的,它在內部進行add、remove、set、get等操作時使用了Synchronized關鍵字對容器對象進行了加鎖操作,保證了併發安全性。但它的一個缺點就是併發程度低,因爲它對讀寫等操作都進行了加鎖,導致了多個不同線程之間進行讀讀、讀寫、寫讀、寫寫的操作時除擁有鎖的線程,其它線程都阻塞。也就是說只要有一個線程在修改或者插入或者刪除或者讀該容器,其它線程就不能對這個線程做修改或者插入或者刪除或者讀操作,因爲它用synchronized關鍵字修飾的方法在執行時,會用鎖鎖住這個容器對象;其它線程要訪問該容器對象時,需要使用鎖,發現鎖被佔用,就只能阻塞等待。

//synchronized修飾實例方法,鎖住的是容器對象
 public synchronized E set(int index, E element) {
        if (index >= this.elementCount) {
            throw new ArrayIndexOutOfBoundsException(index);
        } else {
            E oldValue = this.elementData(index);
            this.elementData[index] = element;
            return oldValue;
        }
    }

CopyOnWriteArrayList(線程安全)

底層結構: 動態數組

CopyOnWriteArrayList容器是線程安全版的ArrayList,它通過使用volatile關鍵字和synchronized關鍵字來保證併發安全。它的併發程度比Vector更高,因爲它只在寫操作(包括修改、插入、刪除等操作)上加了鎖,而且鎖住的是容器內的一個常量對象,而不是容器對象,而讀操作不加鎖,因此它導致寫操作和讀操作之間不產生衝突。在進行寫操作時不是對容器內的元素進行修改,而是將容器內的元素複製一份,對副本進行修改,因此不影響其它線程讀取正確的數據。因此它導致的只是寫寫操作之間的阻塞(寫操作需要搶該常量對象的鎖),而讀讀、讀寫、寫讀等操作之間不會相互阻塞,以此提高了併發程度。它的思想與數據庫的MVCC差不太多,類似於多版本併發控制。

 final transient Object lock = new Object();
public boolean add(E e) {
        synchronized(this.lock) {  //注意它鎖的是常量對象,而不是容器對象
            Object[] es = this.getArray();
            int len = es.length;
             //添加元素是將原數組進行復制,產生一份副本,將數組添加到副本數組,再將副本數組替換原數組,使得其它線程正確讀數據    
            es = Arrays.copyOf(es, len + 1);
            es[len] = e;
            this.setArray(es);
            return true;
        }
    }

LinkedList(線程不安全)

底層結構: 雙向鏈表
在這裏插入圖片描述
LinkedList因爲底層結構是鏈表,因此它的特點是存儲密度小(因爲部分空間用來存儲指針),刪除或者插入效率高,查找效率低,不支持高效的隨機元素訪問。

LinkedList容器實現了Deque接口,Deque是Queue的子接口,因此LinkedList可以用來作爲隊列使用,如下:

Deque<String> que=new LinkedList<>();

List類容器小結

初始時,不知給數組分配多大內存時,可以選擇使用List類容器,當主要用來進行隨機查找而插入刪除操作較少時,可以選擇使用順序結構的List容器,如ArrayList和Vector、CopyOnWriteArrayList。在單線程環境使用ArrayList,多線程環境推薦使用CopyOnWriteArrayList,併發性能更好。若是插入和刪除操作比較多,推薦使用鏈表結構,例如LinkedList。

容器是否實現RandomAccess 接口表明容器是否支持高效的隨機訪問能力,實現了 RandomAccess 接口的list,優先選擇普通 for 循環 ,其次 foreach,未實現 RandomAccess接口的list,優先選擇iterator遍歷(foreach遍歷底層也是通過iterator實現的),大size的數據,千萬不要使用普通for循環

Queue

隊列是數據結構中的一大類,一般用於緩衝、併發訪問,主要特徵是元素先進先出( FIFO),好比排隊取錢,我先來我就能排在前面,就能先取到錢先走人。隊列在數據結構中可以被分爲兩種,一種是單隊列,一種是循環隊列,循環隊列主要是防止隊列的假溢出問題(隊列中有位置,卻無法加入新元素)。隊列的實現有挺多種的,這裏不進行詳細敘述,只做簡要說明。隊列的實現可以分爲兩類,一類線程不安全的,例如LinkedList、PriorityQueue ;另一類是線程安全的,線程安全的又可分爲阻塞隊列和非阻塞隊列,阻塞隊列可以通過加鎖來實現,非阻塞隊列可以通過 CAS 操作實現。非阻塞隊列的典型例子是 ConcurrentLinkedQueue;而阻塞隊列主要的就是阻塞隊列接口BlockingQueue接口的實現類,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue(PriorityQueue的線程安全版)。

單隊列
在這裏插入圖片描述

循環隊列
在這裏插入圖片描述

棧也是數據結構中的一大類,主要特徵是後進先出(LIFO),在Java中主要提供了Stack類用於對棧的實現,它是Vector的子類,基於Vector實現,因此它的底層結構也是動態數組,是線程安全的。

其它常用容器

BitSet(線程不安全)

底層結構: long類型的數組(long[] words)

BitSet是一個只存儲0/1值的位數組,它是基於long類型數組實現的,因此它的長度是64的倍數(初始時分配64位),自動擴容後依然是64的倍數。BitSet常見的應用場景是對海量數據的處理,用於大數據量的查找、去重、統計、排序、判別以及求數據的並集、交集、補集等,還可以用於日誌分析、用戶數統計等統計工作。

常用方法如下:

//創建一個默認的對象(64爲),所有位初始化爲 false
BitSet();
//允許用戶指定初始大小,所有位初始化爲 false
BitSet(int nbits);
//a = a & b
void and(BitSet set);        
//a = a & !b
void andNot(BitSet set);
//a = a^b
void xor(BitSet set);
//a = a | b
void or(BitSet set);
//將指定索引處的位設置爲 true
void set(int bitIndex)        
//將指定索引處的位設置爲指定的值
void set(int bitIndex, boolean value);          
//將指定的 fromIndex(包括)到指定的 toIndex(不包括)範圍內的位設置爲 true
void set(int fromIndex, int toIndex);        
//將指定的 fromIndex(包括)到指定的 toIndex(不包括)範圍內的位設置爲指定的值
void set(int fromIndex, int toIndex, boolean value);
//返回指定索引處的位值
boolean get(int bitIndex);       
//返回一個新的 BitSet,它由 fromIndex(包括)到 toIndex(不包括)範圍內的位組成
BitSet get(int fromIndex, int toIndex);
//返回此 BitSet 的“邏輯大小”,即實際使用的位數
int length();         
//返回此 BitSet 表示位值時實際使用空間的位數,即 words.length * 64
int size();  
//將此 BitSet 中的所有位設置爲 false
void clear();        
//將索引指定處的位設置爲 false
void clear(int bitIndex);       
//將指定的 fromIndex(包括)到指定的 toIndex(不包括)範圍內的位設置爲 false
void clear(int fromIndex, int toIndex);         
//將指定索引處的值設置爲其當前值的補碼
void flip(int bitIndex);          
//將 fromIndex(包括)到指定的 toIndex(不包括)範圍內的每個位設置爲其當前值的補碼
void flip(int fromIndex, int toIndex);         
//返回此 BitSet 中設置爲 true 的位數
int cardinality();       
//如果指定 BitSet 中設置爲 true 的位,在此 BitSet 中也爲 true,則返回 ture
boolean intersects(BitSet set);          
//如果此 BitSet 中沒有包含任何設置爲 true 的位,則返回 ture
boolean isEmpty();        
//返回 fromIndex(包括)之後第一個設置爲 false 的位的索引
int nextClearBit(int fromIndex);
//返回 fromIndex(包括)之後的第一個設置爲 true 的位的索引
int nextSetBit(int fromIndex);        
//返回該 BitSet 中爲 true 的索引的字符串拼接形式
String toString();
//返回 hashcode 值
int hashcode();
//複製此 BitSet,生成一個與之相等的新 BitSet。
Object clone();
//將此對象與指定的對象進行比較。
boolean equals(Object obj);

感興趣者可以翻閱以下兩篇博客或者自行搜索,這裏不做過多詳述。
BitSet實現原理
BitSet的應用

Pair(線程不安全)

Pair與Map有點類似,都是一次可以存儲兩個元素對象,但它比Map底層結構簡單的不要太多,Pair可以存儲兩個值(Key,Value),但它其實沒有鍵與值之分,也不能通過鍵獲得值,只是個單純的用於存儲兩個值的容器類。讀者可以自行去閱讀源碼,源碼極其簡單。應用場景是隻是單純的需要容器存儲兩個元素對象。

總結

各類容器沒有好壞之分,只有是否適用之分,當需要根據鍵值獲取到元素值時就選用Map接口下的集合,當要求容器內不能出現重複元素時,使用Set接口下的集合,一般情況下就選擇List接口下的集合。選擇容器類別之後再根據需求選擇具體的容器。

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