1. 程式人生 > >Java集合類總結

Java集合類總結

tree dex trac emp oat err 條件 最終 一個地方

集合類和接口之間的關系圖,能夠比較清楚的展示各個類和接口之間的關系(其中:點框為接口(...) 短橫線框為抽象類(---) 實線為類)

技術分享

上圖可以看到:集合可以分成兩部分來學習。一個是以Collection為頂層接口,這種集合是單值元素<value>。一個是以Map為頂層接口,這種結合是<key,value>形式。

List和Set

Collection接口下面直接繼承的有抽象類 AbstractCollection和兩個接口:List、Set 相同點: 1、提供大致相同的操作數據集合的方法,例如:add()、remove()、congtains()、size()等 不同點: 1、List元素可重復、元素有序(和添加順序一致)、元素可為空
2、Set元素不可重復、元素無序、元素可為空 思考:可空和不可為空的限定條件是什麽? List子類:ArrayList、LinkedList、Vector ArrayList底層數據結構是數組,LinkedList底層數據結構是鏈表。Vecotor和ArrayList差不多,但是線程安全,這也就意味著它速率比ArrayList慢。 數組和鏈表的數據結構特點: 數組:下標訪問,所以訪問速度比較快,TC為O(1),插入的時候需要移動數據元素,插入慢。TC為O(n) 鏈表:插入速度快T(1)。只需要改變指針指向。訪問慢O(n),需要從頭開始一個一個查找。 Set子類:HashSet、TreeSet
HashSet基於HashMap的key實現,元素不重復。TreeSet實現了SortedSet,基於TreeSet的key實現,意味著有序,當然也是不重復。 HashSet唯一性保證: 基於HashMap的key實現,當通過hash()方法計算出要插入的Entry[]數組的位置後,通過equals()方法來進行比較,equals()方法比較的是兩個對象的引用是否相等,如果不相等,則插入,如果相等,則把這個值覆蓋上一個。 TreeSet有序性保證: 如果簡單類型,由於他們繼承了Comparable,直接比較,如果為自定義對象,則需要顯示實現Comparable,實現comparableTo()方法,方法內規則自己定義。

Map之HashMap和TreeMap

HashMap key和value值允許為空,key相同時,前者會覆蓋後者,保持最新。 Comparable和Comparator 可以看到在TreeMap裏面有兩套比較的方法,分別使用了Comparable和Comparator。if...else分別使用兩種方式。所以對於集合排序,二者都可以使用。
 1 Comparator<? super K> cpr = comparator;
 2         if (cpr != null) {
 3             do {
 4                 parent = t;
 5                 cmp = cpr.compare(key, t.key);
 6                 if (cmp < 0)
 7                     t = t.left;
 8                 else if (cmp > 0)
 9                     t = t.right;
10                 else
11                     return t.setValue(value);
12             } while (t != null);
13         }
14         else {
15             if (key == null)
16                 throw new NullPointerException();
17             @SuppressWarnings("unchecked")
18                 Comparable<? super K> k = (Comparable<? super K>) key;
19             do {
20                 parent = t;
21                 cmp = k.compareTo(t.key);
22                 if (cmp < 0)
23                     t = t.left;
24                 else if (cmp > 0)
25                     t = t.right;
26                 else
27                     return t.setValue(value);
28             } while (t != null);
29         }
Comparable是在集合內部定義的方法的排序,像Integer,String等都會實現Comparable接口,所以天生具有可比較的屬性。 Comparator是在集合外部定義的方法,如果排序規則不能滿足需要,可以顯示實現該接口,然後重寫方法定義規則,當然重寫Comparable也是可以的。 使用Comparator是策略模式,就是不改變策略本身,而是通過一個策略對象來改變他的行為。(策略模式需要復習 比較兩個對象的值,使用兩種接口的實現方式如下: Comparable
int result = person1.comparTo(person2)

Comparator

PersonComparator   comparator=   new   PersonComparator();
comparator.compare(person1,person2);

Collections

Collections是一個類,區別於Collection接口。Collections提供了幾個操作集合對象的方法,同時提供了幾個靜態共有常量。 常量如下: 技術分享 這些常量可以在參數判斷時使用。例如:
1 Map getMap(String key){
2         if(StringUtil.isEmpty(key)){
3             return Collections.EMPTY_MAP;
4         }
5 ... ...
6 }
幾個比較常用的方法:sort()、addAll()、isEmpty()等,重點看sort()方法
1 public static <T extends Comparable<? super T>> void sort(List<T> list) {
2         list.sort(null);
3 }
sort()方法主要用來對List排序,因為Set無序。 這裏主要看對於ArrayList的排序。因為它比較常用。
1 public void sort(Comparator<? super E> c) {
2         final int expectedModCount = modCount;
3         Arrays.sort((E[]) elementData, 0, size, c);
4         if (modCount != expectedModCount) {
5             throw new ConcurrentModificationException();
6         }
7         modCount++;
8     }
這裏面調用了Arrays.sort()方法,點進去看到函數是這樣的:
 1 public static <T> void sort(T[] a, int fromIndex, int toIndex,
 2                                 Comparator<? super T> c) {
 3         if (c == null) {
 4             sort(a, fromIndex, toIndex);
 5         } else {
 6             rangeCheck(a.length, fromIndex, toIndex);
 7             if (LegacyMergeSort.userRequested)
 8                 legacyMergeSort(a, fromIndex, toIndex, c);
 9             else
10                 TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);
11         }
12     }
這裏看到,如果是LegacyMergeSort.userRequested
1 static final class LegacyMergeSort {
2         private static final boolean userRequested =
3             java.security.AccessController.doPrivileged(
4                 new sun.security.action.GetBooleanAction(
5                     "java.util.Arrays.useLegacyMergeSort")).booleanValue();
6     }
這裏理解為是一個配置,通過傳參數來設置使用傳統的歸並排序。否則的話,使用TimSort排序,這個是歸並排序的優化版,也就是說,默認使用TimSort排序。 歸並排序:大致思路就是先把待排序數組依次分解,然後遞歸兩兩組合排序,直至整個數組有序。時間復雜度O(N*logN). 下面兩個圖就比較直觀反映了數據處理過程 技術分享

技術分享

Collection是個接口,然後JDK提供了Collections類來對接口通用方法進行操作。類似的還有Arrays工具類。以後自己在設計接口的時候也可以考慮此種方式。

源碼-HashMap

HashMap源碼關鍵點:
  • 數組+鏈表(JDK1.7及以前)
  • 數組+紅黑樹(JDK1.8)
  • 一個元素的添加過程
  • 線程不安全的解釋
1、數組+鏈表(JDK1.7及以前) 技術分享 這裏畫一個圖,有點不恰當的是Entry[]裏面應該存放鏈表的第一個元素。這裏直接畫到了外面。 Entry數組結構如下:
1 static class Entry<K,V> implements Map.Entry<K,V> {
2         final K key;
3         V value;
4         Entry<K,V> next;
5         int hash;
6 }
2、數組+紅黑樹(JDK1.8) 技術分享 3、一個元素的添加過程 HashMap的特點是key不能重復,key-value值均可為空,如果key為null,hash值返回0。有一個地方需要註意的是:key相等和key通過hash之後相等的區別。也就是說,如果key1=key2,那麽通過hash之後的值也相等,插入同一個Entry數組對應的下標中。然後會通過equals()方法判斷二者是否相等,如果相等,則後者覆蓋前者保持最新。如果不相等,則把該元素插入到Entry元素根節點位置,原節點往後移動作為該節點的下一個節點或者子節點(對於樹)。 4、線程不安全的地方 (1)put操作 當有A、B兩個線程Hash之後同時到達Entry數組的下表 i 時,就有可能出現問題。先來看一下一個線程hash之後的插入操作,其實就是一個鏈表的插入過程。 技術分享 詳細步驟: 1.oldValue = Entity[4]; 2.Entity[4] = newValue; 3.newValue.next = oldValue; 那麽在多線程操作情況下,可能線程1在“2”步執行完成後,還沒執行“3”步時,線程2執行了containsKey方法,這時就取不到"oldValue"。再過幾毫秒去看,又有"oldValue"了。 (2)resize操作 一個線程在hash找下標,一個在擴容,出現問題。

源碼-ConcurrentHashMap

ConcurrentHashMap源碼關鍵點:
  • 它是線程安全的,如何保證
  • 分段鎖相比Hashtable整個Synchronized方法的優勢
  • 分段鎖的時候,size()方法的處理
ConcurrentHashMap和Hashtble的key和value值都是不能為空的,源碼中put操作時首行有為空判斷
if (key == null || value == null) throw new NullPointerException();
Hashtable是線程安全的,保證線程安全的方法是在每個方法上面加上Synchronize。
synchronized int size();
synchronized boolean isEmpty();
... ...
相比Hashtable,ConcurrentHashMap采用了分段鎖(Segment)來保證線程安全性。每個段相當於是一個小的Hashtable。但同時會存在多個Segment(默認16),這樣,每個段之間的操作是真正的並發而彼此之間不受影響。 技術分享 從上圖可以看到:一個ConcurrentHashMap包含多個segment,每個segment都是一把鎖,對應一段table(這個table就是HashMap的結構,由HashBucket【hash桶】和HashEntry組成)。各個segment之間是彼此獨立的。每個Segment均繼承了可重復鎖ReetrantLock
static final class Segment<K,V> extends ReentrantLock implements Serializable
Segment下面包含很多歌HashEntry列表數組。對於一個key,需要經過三次hash操作,才能最終定位這個元素的位置。三次hash分別是: 1、對於一個key,先進行一次hash操作,得到hash值h1,即h1=hash1(key); 2、將得到的h1的高幾位進行第二次hash,得到hash值h2,即h2=hash2(h1高幾位),通過h2能確定該元素放在哪個Segment中; 3、將得到的h1進行hash,得到hash值h3,即h3=hash3(h1),通過h3能夠確定該元素放置在哪個HashEntry。 CoucurrentHashMap中主要實體類就三個:ConcurrentHashMap(整個Hash表),Segment(片段),HashEntry(節點) 不變(Immutable)和易變(Volatile) ConcurrentHashMap允許多個讀操作並發進行,讀操作不需要加鎖。如果使用傳統的技術,如HashMap中的實現,如果允許可以在hash鏈的中間添加或刪除元素,讀操作將得到不一致的數據。ConcurrentHashMap實現技術是保證HashEntry幾乎是不可變的。HashEntry代表每個hash鏈中的一個節點,結構如下:
1 static final class HashEntry<K,V> {  
2      final K key;  
3      final int hash;  
4      volatile V value;  
5      volatile HashEntry<K,V> next;  
6  }
在JDK1.6中,HashEntry中的next指針也定義為final,並且每次插入將添加新節點作為鏈的頭節點,每次刪除節點時,會將刪除節點之前(這裏指時間的前後,不是指位置)的所有節點拷貝一份組成一個新的鏈,而將當前節點的上一個節點的next指針指向當前節點的下一個節點,從而在刪除之後有兩條鏈存在,因而可以保證即使在同一條鏈中,有一個線程正在刪除,而另一個線程正在遍歷,他們都能工作良好,因為遍歷的線程能繼續使用原有的鏈。因而這是一種更加細粒度的happens-before關系,即如果遍歷線程在刪除線程結果後開始,則它能看到刪除後的變化,如果它發生在刪除線程正在執行中間,則它會使用原有的鏈,而不會等到刪除線程結束執行後再執行,也就是說,這種情況下遍歷線程看不到刪除線程的影響。而HashMap中的Entry只有key是final的
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}
不變模式是多線程安全裏最簡單的一種保障方式。因為你拿他沒有辦法,想改變它也沒有機會。 不變模式主要通過final關鍵字來限定。Final域使得確保初始化安全性成為可能。 下面主要通過對ConcurrentHashMap的初始化、put操作、get操作、size操作和containsValue操作進行分析和學習。 初始化 構造函數的源碼如下:
 1 public ConcurrentHashMap(int initialCapacity,
 2                              float loadFactor, int concurrencyLevel) {
 3         if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
 4             throw new IllegalArgumentException();
 5         if (concurrencyLevel > MAX_SEGMENTS)
 6             concurrencyLevel = MAX_SEGMENTS;
 7         // Find power-of-two sizes best matching argumentsint sshift = 0;
 8         int ssize = 1;
 9         while (ssize < concurrencyLevel) {
10             ++sshift;
11             ssize <<= 1;
12         }
13         this.segmentShift = 32 - sshift;
14         this.segmentMask = ssize - 1;
15         if (initialCapacity > MAXIMUM_CAPACITY)
16             initialCapacity = MAXIMUM_CAPACITY;
17         int c = initialCapacity / ssize;
18         if (c * ssize < initialCapacity)
19             ++c;
20         int cap = MIN_SEGMENT_TABLE_CAPACITY;
21         while (cap < c)
22             cap <<= 1;
23         // create segments and segments[0]
24         Segment<K,V> s0 =
25             new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
26                              (HashEntry<K,V>[])new HashEntry[cap]);
27         Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
28         UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]this.segments = ss;
29     }
傳入的參數有initialCapacity(初始容量),loadFactor(負載因子),concurrencyLevel(並發級別)三個參數。 1、initialCapacity指的是ConcurrentHashMap中每條鏈中的Entry的數量。默認值static final int DEFAULT_INITIAL_CAPACITY = 16; 2、loadFactor表示負載因子,當ConcurrentHashMap中元素大於loadFactor*最大容量時需要擴容。(最大容量=每條鏈的entry格式*Entry的數組長度) 3、concurrencyLevel(並發級別)用來確定Segment的個數,Segment的個數是大於等於concurrencyLevel的第一個2的n次方的數。比如,如果concurrencyLevel為12,13,14,則segment的數目就為16(2的四次方)。默認值static final int DEFAULT_CONCURRENCY_LEVEL = 16。理想情況下ConcurrentHashMap的真正的並發訪問量能夠達到concurrencyLevel(這種情況就是訪問的數據恰好分別落在不同的Segment中,而這些線程能夠無競爭的自由訪問) put操作 put操作的源碼如下:
public V put(K key, V value) {
      Segment<K,V> s;
      if (value == null)
          throw new NullPointerException();
      int hash = hash(key);
      int j = (hash >>> segmentShift) & segmentMask;
      if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
           (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
          s = ensureSegment(j);
      return s.put(key, hash, value, false);
  }
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        elsesetEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
put操作需要加鎖。 get操作 get操作不需要加鎖(如果value為null,會調用readValueUnderLock,只有這個步驟會加鎖),通過volatile和final來確保數據安全。 size()操作 size()操作與put和get操作的最大區別在於,size操作需要遍歷所有的Segment才能算出整個Map的大小,而put和get只關系一個Segment。假設我們當前遍歷的Segment為SA,那麽在遍歷SA過程中其他的Segment比如SB可能會被修改,於是這一次運算出來的size值可能並不是Map當前的真正大小。一個比較簡單的辦法就是計算Map大小的時候所有的Segment都LOCK住,不能更新(包括put,removed等)數據,計算完之後再UNLOCK。這是普通人所能想到的方案,但是牛逼的作者還有一個更好的Idea:先給3次機會,不lock所有的segment,遍歷所有segment,累加各個segment的大小得到整個Map大小,如果某相鄰的兩次計算獲取的所有Segment的更新次數(每個Segment都有一個modCount變量,這個變量在Segment中的Entry被修改時會加1,通過這個值可以得到每個Segment的更新操作的次數)是一樣的,說明計算過程中沒有更新操作(至少從查看Map的大小方向來看沒有變化),則直接返回這個值。如果這三次不加鎖的計算過程中Map的更新次數有變化,則之後的計算先對所有的Segment加鎖,再遍歷所有Segment計算Map的大小,最後再解鎖所有的Segment。源代碼如下:
public int size() {
        // Try a few times to get accurate count. On failure due to// continuous async changes in table, resort to locking.final Segment<K,V>[] segments = this.segments;
        int size;
        boolean overflow; // true if size overflows 32 bitslong sum;         // sum of modCountslong last = 0L;   // previous sumint retries = -1; // first iteration isn‘t retrytry {
            for (;;) {
                if (retries++ == RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        ensureSegment(j).lock(); // force creation
                }
                sum = 0L;
                size = 0;
                overflow = false;
                for (int j = 0; j < segments.length; ++j) {
                    Segment<K,V> seg = segmentAt(segments, j);
                    if (seg != null) {
                        sum += seg.modCount;
                        int c = seg.count;
                        if (c < 0 || (size += c) < 0)
                            overflow = true;
                    }
                }
                if (sum == last)
                    break;
                last = sum;
            }
        } finally {
            if (retries > RETRIES_BEFORE_LOCK) {
                for (int j = 0; j < segments.length; ++j)
                    segmentAt(segments, j).unlock();
            }
        }
        return overflow ? Integer.MAX_VALUE : size;
    }
舉個栗子: 一個Map中有四個Segment,標記為S1,S2,S3,S4。現在我們要獲取Map的size。計算過程是這樣的:
  • 第一次計算,不對S1,S2,S3,S4加鎖,遍歷所有的Segment,假設每個Segment的大小分別為1,2,3,4,更新操作次數分為別2,2,3,1,則這次計算可以得到Map的總大小為1+2+3+4=10,總共更新次數為8;
  • 第二次計算,不對S1,S2,S3,S4加鎖,遍歷所有的Segment,假設這次每個Segemnt的大小變成了2,2,3,4,更新次數分別為3,2,3,1,因為兩次計算得到的Map更新次數不一致(第一次是8,第二次是9),則可以斷定這段時間Map數據被更新。
  • 第三次計算,不對S1,S2,S3,S4加鎖,遍歷所有的Segment,假設每個Segment的更新操作次數還是3,2,3,1,則因為第二次計算和第三次計算得到的Map更新次數是一致的,就能說明第二次計算和第三次計算這段時間內Map數據沒有被更新,此時可以直接返回第三次計算得到的Map大小。
最壞的情況:第三次計算得到的數據更新次數和第二次計算也不一樣,則只能先對所有的Segment加鎖再計算最後解鎖。 containsValue containsValue操作采用了和size操作一樣的想法。

Java集合類總結