Java集合大整理

此處爲整理,更詳細的源碼分析請查閱 JDK源碼分析其他文章

爲了適應csdn的窗口大小,表格嚴重變形了。。。


null
值重複
底層實現
擴容
增、刪、迭代
包含
備註
HashSet
允許,just 1個
no
HashMap
同HashMap
【add】:調用HashMap的put方法,put的value傳入僞值static final Object PRESENT = new Object(),僅僅爲了保持映射關係;(所有value都是同一個對象
【remove】:調map的remove
有contains,
無get
HashMap中的Key是根據對象的hashCode() 和 euqals()來判斷是否唯一的。
So:爲了保證HashSet中的對象不會出現重複值,在被存放元素的類中必須要重寫hashCode()和equals()這兩個方法。
TreeSet
No,
add-null空指針異常
no
【TreeMap】,
實現了NavigableMap接口,
一種SortedMap
樹結構,無擴容一說
add調用TreeMap的put方法,同樣有PRESENT
remove方法。
有contains,
無get
默認使用元素的自然順序對元素進行排序;
可重寫compare方法實現對象的自定義排序。
EnumSet
No,
add-null空指針異常

no
long(數組)、
位運算
Enum元素固定,無擴容
add:數組賦值;
remove:置null
有contains,
無get
判斷是否包含null和removenull無異常;remove-null時返回false
EnumMap
No,
add-null空指針異常;
value可以爲null
key不重複
transient數組
Enum元素固定,無擴容
put:數組賦值;
remove:置null
containsKey;
containsValue。
創建時必須指定key類型;
元素順序爲Enum的順序;
迭代時不會拋出ConcurrentModificationException;
NULL和null的區別。
HashMap
key、value均允許null,
但key僅允許1個null。
key不重複,
value可以

位桶+鏈表/紅黑樹
size > tab.length*threshold;
newCap = oldCap<<1;
新容量:2倍擴容
put、remove;
迭代時remove拋ConcurrentModificationException;注意正確迭代方式
containsKey;
containsValue。


LinkedHashMap
同HashMap 同HashMap
HashMap+雙向鏈表
同HashMap
put、remove;

注意get模式;
contains調用HashMap的containsKey;
containsValue(遍歷鏈表)
     像hashMap一樣用table儲存元素【桶位依舊分散,和HashMap的存放位置相同】,put時直接調用的是HashMap的put方法。
      
TreeMap
Key不允許null;
value允許。
同HashMap





ArrayList
允許null,隨意
允許重複
數組
初始容量10,
grow1.5倍

contains判斷元素存在

LinkedList
ArrayList
ArrayList
基於鏈表的數據結構

remove只移除第一個;
迭代時remove拋ConcurrentModificationException(有特例,元素個數<=2);
有contains,get


ConcurrentHashMap
key、value均不允許,put-null空指針異常;
同HashMap
HashMap+CAS無鎖算法
實際容量>=sizeCtl,則擴容
用foreach迭代,Map定義時必須制定key-value類型,否則cant convert
containsKey、
containsValue


允許null:HashMap和以其爲底層結構的非同步集合;List
ArrayList相關






有序:
    先說明有序的概念:迭代輸出和存入順序一致即爲有序(可以理解爲先進先出FIFO)(注:Java8支持list逆序迭代,我們討論有序時忽略這個)
    不要和TreeSet弄混了,TreeSet所謂的“有序”,指的是內部存儲結構有特定的存儲規則,它默認使用元素的自然順序對元素進行排序,卻打亂了元素的存入順序。So,嚴格來講,TreeSet是無序的。
隨機訪問:
    即使用[]操作符訪問其中的元素。

EnumMap:

Set keySet = enumMap.keySet();

Iterator iteKey = keySet.iterator();

while(iteKey.hasNext()){

    Object object =(Object) iteKey.next();

    System.out.print(object +"="+ enumMap.get(object)+"; ");

}

Collection<Object> vals = enumMap.values();
Set<Entry<Season, Object>> entrySet = enumMap.entrySet();

HashMap:
  • 當某個桶中的鍵值對數量大於8個【9個起】,且桶數量大於等於64,則將底層實現從鏈表轉爲紅黑樹 ;
  • int threshold; // 新的擴容resize臨界值,當實際大小(容量*填充比)大於臨界值時,會進行2倍擴容;
  • key是有可能是null的,並且會在0桶位位置;
  • tableSizeFor(int cap) { //計算下次需要調整大小的擴容resize臨界值;結果爲>=cap的最小2的自然數冪(64-》64;65-》128)
  •  length爲2的整數冪保證了length - 1 最後一位(二進制表示)爲1,從而保證了索引位置index即( hash &length-1)的最後一位同時有爲0和爲1的可能性,保證了散列的均勻性。length爲2的冪保證了按位與最後一位的有效性,使哈希表散列更均勻。
  • resize】時【鏈表】的變化: 元素位置在【原位置】或【原位置+oldCap】
  • 鏈表轉紅黑樹後,【僅在擴容resize時】若樹變短,會恢復爲鏈表。

LinkedHashMap:
  • remove後再put,集合結構變化:只要未衝突,table不改變(想想put原理就好理解了);但鏈表改變,新元素始終在tail。
  • 顯式地指定爲access order後【前提】,調用get()方法,導致對應的entry移動到雙向鏈表的最後位置(tail),但是table未沒變。
  • So LinkedHashMap元素有序存放,但並不保證其迭代順序一直不變
  • LinkedHashMap的每個bucket都存了這個bucket的before和after,且每個before(after)又存儲了自身的前驅後繼,直到null。
迭代:
    Iterator<Map.Entry> iterl = map.entrySet().iterator();
    利用ArrayList的【ListIterator】向前迭代:
        ListIterator<Map.Entry> iterpre = new ArrayList<Map.Entry>(map.entrySet()).listIterator(map.size());
        while (iterpre.hasPrevious()) {……}

ArrayList:
  • int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍(15 >> 1=7) 擴容是大約1.5倍擴容,HashMap則是剛好2倍擴容。
  • add(int index, E element);將當前處於該位置的元素(如果有的話)和所有後續元素向後移動(其索引加 1)【System.arraycopy】。
  • trimToSize()去掉預留元素的位置,返回一個新數組,新數組不含null,數組的size和elementData.length相等,以節省空間。此函數可避免size很小但elementData.length很大的情況。
  • 不建議使用contains(Object o)方法,看源碼就知道了,調用其內置的indexOf方法,for循環一個個equals,這效率只能呵呵噠了,建議使用hashcode。
  • remove: 首先判斷要remove的元素是null還是非null,然後for循環查找,核心是fastRemove(index)方法。 fastRemove並不返回被移除的元素。  elementData[--size] = null;因爲arraycopy方法是將elementData的index+1處開始的元素往前複製,也就是說最後一個數本該消除,但還在那裏,所以需要置空。
  • subList方法得到的subList將和原來的list互相影響,不管你改哪一個,另一個都會隨之改變,而且當父list結構改變時,子list會拋ConcurrentModificationException異常。解決方案:List<String> subListNew = new ArrayList(parentList.subList(1, 3));【類似Arrays.asList()方法】

ConcurrentHashMap:
  • CAS算法;unsafe.compareAndSwapInt(this, valueOffset, expect, update);  CAS(Compare And Swap),意思是如果valueOffset位置包含的值與expect值相同,則更新valueOffset位置的值爲update,並返回true,否則不更新,返回false。
  • 與Java8的HashMap有相通之處,底層依然由“數組”+鏈表+紅黑樹;
  • 底層結構存放的是TreeBin對象,而不是TreeNode對象;
  • CAS作爲知名無鎖算法,那ConcurrentHashMap就沒用鎖了麼?當然不是,hash值相同的鏈表的頭結點還是會synchronized上鎖。 

private transient volatile int sizeCtl;

sizeCtl是控制標識符,不同的值表示不同的意義。

  • 負數代表正在進行初始化或擴容操作 
  • -1代表正在初始化 
  • -N 表示有N-1個線程正在進行擴容操作 
  • 正數或0代表hash表還沒有被初始化,這個數值表示初始化或下一次進行擴容的大小,類似於擴容閾值。它的值始終是當前ConcurrentHashMap容量的0.75倍,這與loadfactor是對應的。實際容量>=sizeCtl,則擴容。
 concurrencyLevel
    能夠同時更新ConccurentHashMap且不產生鎖競爭的最大線程數,在Java8之前實際上就是ConcurrentHashMap中的分段鎖個數,即Segment[]的數組長度正確地估計很重要,當低估,數據結構將根據額外的競爭,從而導致線程試圖寫入當前鎖定的段時阻塞;相反,如果高估了併發級別,你遇到過大的膨脹,由於段的不必要的數量; 這種膨脹可能會導致性能下降,由於高數緩存未命中。
        在Java8裏,僅僅是爲了兼容舊版本而保留。唯一的作用就是保證構造map時初始容量不小於concurrencyLevel。
ForwardingNode:
    並不是我們傳統的包含key-value的節點,只是一個標誌節點,並且指向nextTable,提供find方法而已。生命週期:僅存活於擴容操作且bin不爲null時,一定會出現在每個bin的首位。
3個原子操作(調用頻率很高)

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { // 獲取索引i處Node

    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);

    }

    // 利用CAS算法設置i位置上的Node節點(將c和table[i]比較,相同則插入v)。

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,

                                        Node<K,V> c, Node<K,V> v) {

        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);

    }

    // 設置節點位置的值,僅在上鎖區被調用

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {

        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);

    }

ConcurrentHashMap無鎖多線程擴容,減少擴容時的時間消耗。
transfer擴容操作:單線程構建兩倍容量的nextTable;允許多線程複製原table元素到nextTable。
  1. 爲每個內核均分任務,並保證其不小於16;
  2. 若nextTab爲null,則初始化其爲原table的2倍;
  3. 死循環遍歷,直到finishing。
  • 節點爲空,則插入ForwardingNode;
  • 鏈表節點(fh>=0),分別插入nextTable的i和i+n的位置;【逆序鏈表??】
  • TreeBin節點(fh<0),判斷是否需要untreefi,分別插入nextTable的i和i+n的位置;【逆序樹??】
  • finishing時,nextTab賦給table,更新sizeCtl爲新容量的0.75倍 ,完成擴容。

以上說的都是單線程,多線程又是如何實現的呢?
       遍歷到ForwardingNode節點((fh = f.hash) == MOVED),說明此節點被處理過了,直接跳過。這是控制併發擴容的核心 。由於給節點上了鎖,只允許當前線程完成此節點的操作,處理完畢後,將對應值設爲ForwardingNode(fwd),其他線程看到forward,直接向後遍歷。如此便完成了多線程的複製工作,也解決了線程安全問題。

2、 put相關:

理一下put的流程:
①判空:null直接拋空指針異常;
②hash:計算h=key.hashcode;調用spread計算hash=(^(>>>16))& HASH_BITS;
③遍歷table
  • 若table爲空,則初始化,僅設置相關參數;
  • @@@計算當前key存放位置,即table的下標i=(n - 1) & hash;
  • 若待存放位置爲null,casTabAt無鎖插入;
  • 若是forwarding nodes(檢測到正在擴容),則helpTransfer(幫助其擴容);
  • else(待插入位置非空且不是forward節點,即碰撞了),將頭節點上鎖(保證了線程安全):區分鏈表節點和樹節點,分別插入(遇到hash值與key值都與新節點一致的情況,只需要更新value值即可。否則依次向後遍歷,直到鏈表尾插入這個結點);
  • 若鏈表長度>8,則treeifyBin轉樹(Note:若length<64,直接tryPresize,兩倍table.length;不轉樹)。
④addCount(1L, binCount)。
Note:
1、put操作共計兩次hash操作,再利用“與&”操作計算Node的存放位置。
2、ConcurrentHashMap不允許key或value爲null。
3、addCount(longxintcheck)方法:
    ①利用CAS快速更新baseCount的值;
    ②check>=0.則檢驗是否需要擴容;if sizeCtl<0(正在進行初始化或擴容操作)【nexttable null等情況break;如果有線程正在擴容,則協助擴容】;else if 僅當前線程在擴容,調用協助擴容函數,注其參數nextTable爲null。


 以下爲引用: 

java提高篇(二十)-----集合大家族:http://demo.netfoucs.com/chenssy/article/details/17732841

    6.1、Vector和ArrayList

       1,vector是線程同步的,所以它也是線程安全的,而arraylist是線程異步的,是不安全的。如果不考慮到線程的安全因素,一般用arraylist效率比較高。
       2,如果集合中的元素的數目大於目前集合數組的長度時,vector增長率爲目前數組長度的100%,而arraylist增長率爲目前數組長度的50%.如過在集合中使用數據量比較大的數據,用vector有一定的優勢。
       3,如果查找一個指定位置的數據,vector和arraylist使用的時間是相同的,都是0(1),這個時候使用vector和arraylist都可以。而如果移動一個指定位置的數據花費的時間爲0(n-i)n爲總長度,這個時候就應該考慮到使用linklist,因爲它移動一個指定位置的數據所花費的時間爲0(1),而查詢一個指定位置的數據時花費的時間爲0(i)。

       ArrayList 和Vector是採用數組方式存儲數據,此數組元素數大於實際存儲的數據以便增加和插入元素,都允許直接序號索引元素,但是插入數據要設計到數組元素移動等內存操作,所以索引數據快插入數據慢,Vector由於使用了synchronized方法(線程安全)所以性能上比ArrayList要差,LinkedList使用雙向鏈表實現存儲,按序號索引數據需要進行向前或向後遍歷,但是插入數據時只需要記錄本項的前後項即可,所以插入數度較快!

      6.2、Aarraylist和Linkedlist

       1.ArrayList是實現了基於動態數組的數據結構,LinkedList基於鏈表的數據結構。
       2.對於隨機訪問get和setArrayList覺得優於LinkedList,因爲LinkedList要移動指針。
       3.對於新增和刪除操作add和removeLinedList比較佔優勢,因爲ArrayList要移動數據。
這一點要看實際情況的。若只對單條數據插入或刪除,ArrayList的速度反而優於LinkedList。但若是批量隨機的插入刪除數據,LinkedList的速度大大優於ArrayList. 因爲ArrayList每插入一條數據,要移動插入點及之後的所有數據。

      6.3、HashMap與TreeMap

       1、HashMap通過hashcode對其內容進行快速查找,而TreeMap中所有的元素都保持着某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)。HashMap中元素的排列順序是不固定的)。

       2、 HashMap通過hashcode對其內容進行快速查找,而TreeMap中所有的元素都保持着某種固定的順序,如果你需要得到一個有序的結果你就應該使用TreeMap(HashMap中元素的排列順序是不固定的)。集合框架提供兩種常規的Map實現:HashMap和TreeMap (TreeMap實現SortedMap接口)。

       3、在Map 中插入、刪除和定位元素,HashMap 是最好的選擇。但如果您要按自然順序或自定義順序遍歷鍵,那麼TreeMap會更好。使用HashMap要求添加的鍵類明確定義了hashCode()和 equals()的實現。 這個TreeMap沒有調優選項,因爲該樹總處於平衡狀態。

      6.4、hashtable與hashmap

       1、歷史原因:Hashtable是基於陳舊的Dictionary類的,HashMap是Java 1.2引進的Map接口的一個實現 。

       2、同步性:Hashtable是線程安全的,也就是說是同步的,而HashMap是線程序不安全的,不是同步的 。

       3、值:只有HashMap可以讓你將空值作爲一個表的條目的key或value 。

       七、對集合的選擇

      7.1、對List的選擇

       1、對於隨機查詢與迭代遍歷操作,數組比所有的容器都要快。所以在隨機訪問中一般使用ArrayList

       2、LinkedList使用雙向鏈表對元素的增加和刪除提供了非常好的支持,而ArrayList執行增加和刪除元素需要進行元素位移。

       3、對於Vector而已,我們一般都是避免使用。

       4、將ArrayList當做首選,畢竟對於集合元素而已我們都是進行遍歷,只有當程序的性能因爲List的頻繁插入和刪除而降低時,再考慮LinkedList。

      7.2、對Set的選擇

       1、HashSet由於使用HashCode實現,所以在某種程度上來說它的性能永遠比TreeSet要好,尤其是進行增加和查找操作。

       3、雖然TreeSet沒有HashSet性能好,但是由於它可以維持元素的排序,所以它還是存在用武之地的。




歡迎個人轉載,但須在文章頁面明顯位置給出原文連接;
未經作者同意必須保留此段聲明、不得隨意修改原文、不得用於商業用途,否則保留追究法律責任的權利。

【 CSDN 】:csdn.zxiaofan.com
【GitHub】:github.zxiaofan.com

如有任何問題,歡迎留言。祝君好運!
Life is all about choices! 
將來的你一定會感激現在拼命的自己!

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