1. 程式人生 > >Java集合大整理

Java集合大整理

此處為整理,更詳細的原始碼分析請查閱 

為了適應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使用雙向連結串列實現儲存,按序號索引資料需要進行向前或向後遍歷,但是插入資料時只需要記錄本項的前後項即可