天天看點

備戰-Java 容器

備戰-Java 容器

    玉階生白露,夜久侵羅襪。

簡介:備戰-Java 容器

容器主要包括 Collection 和 Map 兩種,Collection 存儲着對象的集合,而 Map 存儲着key-value 鍵值對(兩個對象)的映射表。

備戰-Java 容器

TreeSet:基于紅黑樹實作,支援有序性操作,例如根據一個範圍查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的時間複雜度為 O(1),TreeSet 則為 O(logN)。

HashSet:基于哈希表實作,支援快速查找,但不支援有序性操作。并且失去了元素的插入順序資訊,也就是說使用 Iterator 周遊 HashSet 得到的結果是不确定的。

LinkedHashSet:具有 HashSet 的查找效率,并且内部使用雙向連結清單維護元素的插入順序。

ArrayList:基于動态數組實作,支援随機通路。

Vector:和 ArrayList 類似,但它是線程安全的。

LinkedList:基于雙向連結清單實作,隻能順序通路,但是可以快速地在連結清單中間插入和删除元素。不僅如此,LinkedList 還可以用作棧、隊列和雙向隊列。

LinkedList:可以用它來實作雙向隊列。

PriorityQueue:基于堆結構實作,可以用它來實作優先隊列。

備戰-Java 容器

TreeMap:基于紅黑樹實作。

HashMap:基于哈希表實作。

HashTable:和 HashMap 類似,但它是線程安全的,這意味着同一時刻多個線程同時寫入 HashTable 不會導緻資料不一緻。它是遺留類,不應該去使用它,而是使用 ConcurrentHashMap 來支援線程安全,ConcurrentHashMap 的效率會更高,因為 ConcurrentHashMap 引入了分段鎖。

LinkedHashMap:使用雙向連結清單來維護元素的順序,順序為插入順序或者最近最少使用(LRU)順序。(LRU算法是Least Recently Used的縮寫,即最近最少使用)

備戰-Java 容器

 Collection 繼承了 Iterable 接口,其中的 iterator() 方法能夠産生一個 Iterator 對象,通過這個對象就可以疊代周遊 Collection 中的元素。

從 JDK 1.5 之後可以使用 foreach 方法來周遊實作了 Iterable 接口的聚合對象。

備戰-Java 容器
備戰-Java 容器

View Code

java.util.Arrays.asList() 可以把數組類型轉換為 List 類型。

值得注意的是 asList() 的參數為泛型的變長參數,不能使用基本類型數組作為參數,隻能使用相應的包裝類型數組。

也可以使用以下方式調用 asList():

如果沒有特别說明,以下源碼分析基于 JDK 1.8。

在 IDEA 中 輕按兩下 shift 鍵調出 Search EveryWhere,查找源碼檔案,找到之後就可以閱讀源碼。

因為 ArrayList 是基于數組實作的,是以支援快速随機通路。RandomAccess 接口辨別着該類支援快速随機通路,其預設數組大小為10

備戰-Java 容器
備戰-Java 容器
備戰-Java 容器

添加元素時使用 ensureCapacityInternal() 方法來保證容量足夠,如果不夠時,需要使用 grow() 方法進行擴容,新容量的大小為 <code>oldCapacity + (oldCapacity &gt;&gt; 1)</code>,即 oldCapacity+oldCapacity/2。其中 oldCapacity &gt;&gt; 1 需要取整,是以新容量大約是舊容量的 1.5 倍左右。(oldCapacity 為偶數就是 1.5 倍,為奇數就是 1.5 倍-0.5)

擴容操作需要調用 <code>Arrays.copyOf()</code> 把原數組整個複制到新數組中,這個操作代價很高,是以最好在建立 ArrayList 對象時就指定大概的容量大小,減少擴容操作的次數。

備戰-Java 容器
備戰-Java 容器

需要調用 System.arraycopy() 将 index+1 後面的元素都複制到 index 位置上,該操作的時間複雜度為 O(N),可以看到 ArrayList 删除元素的代價是非常高的。

備戰-Java 容器
備戰-Java 容器

ArrayList 基于數組實作,并且具有動态擴容特性,是以儲存元素的數組不一定都會被使用,那麼就沒必要全部進行序列化。

儲存元素的數組 elementData 使用 transient 修飾,該關鍵字聲明數組預設不會被序列化。

ArrayList 實作了 writeObject() 和 readObject() 來控制隻序列化數組中有元素填充那部分内容。

備戰-Java 容器
備戰-Java 容器
備戰-Java 容器
備戰-Java 容器

序列化時需要使用 ObjectOutputStream 的 writeObject() 将對象轉換為位元組流并輸出。而 writeObject() 方法在傳入的對象存在 writeObject() 的時候會去反射調用該對象的 writeObject() 來實作序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理類似。

modCount 用來記錄 ArrayList 結構發生變化的次數。結構發生變化是指添加或者删除至少一個元素的所有操作,或者是調整内部數組的大小,僅僅隻是設定元素的值不算結構發生變化。

在進行序列化或者疊代等操作時,需要比較操作前後 modCount 是否改變,如果改變了需要抛出 ConcurrentModificationException。代碼參以上序列化中的 writeObject() 方法。

它的實作與 ArrayList 類似,但是使用了 synchronized 進行同步。

備戰-Java 容器
備戰-Java 容器

Vector 的構造函數可以傳入 capacityIncrement 參數,它的作用是在擴容時使容量 capacity 增長 capacityIncrement。如果這個參數的值小于等于 0,擴容時每次都令 capacity 為原來的兩倍。

備戰-Java 容器
備戰-Java 容器
備戰-Java 容器
備戰-Java 容器

調用沒有 capacityIncrement 的構造函數時,capacityIncrement 值被設定為 0,也就是說預設情況下 Vector 每次擴容時容量都會翻倍。

備戰-Java 容器
備戰-Java 容器

Vector 是同步的,是以開銷就比 ArrayList 要大,通路速度更慢。最好使用 ArrayList 而不是 Vector,因為同步操作完全可以由程式員自己來控制;

Vector 每次擴容請求其大小的 2 倍(也可以通過構造函數設定增長的容量),而 ArrayList 是 1.5 倍。

可以使用 <code>Collections.synchronizedList();</code> 得到一個線程安全的 ArrayList。

也可以使用 concurrent 并發包下的 CopyOnWriteArrayList 類。

寫操作在一個複制的數組上進行,讀操作還是在原始數組中進行,讀寫分離,互不影響。

寫操作需要加鎖,防止并發寫入時導緻寫入資料丢失。

寫操作結束之後需要把原始數組指向新的複制數組。

備戰-Java 容器
備戰-Java 容器

CopyOnWriteArrayList 在寫操作的同時允許讀操作,大大提高了讀操作的性能,是以很适合讀多寫少的應用場景。

但是 CopyOnWriteArrayList 有其缺陷:

記憶體占用:在寫操作時需要複制一個新的數組,使得記憶體占用為原來的兩倍左右;

資料不一緻:讀操作不能讀取實時性的資料,因為部分寫操作的資料還未同步到讀數組中。

是以 CopyOnWriteArrayList 不适合記憶體敏感以及對實時性要求很高的場景。

基于雙向連結清單實作,使用 Node 存儲連結清單節點資訊。

備戰-Java 容器
備戰-Java 容器

每個連結清單存儲了 first 和 last 指針:

備戰-Java 容器

ArrayList 基于動态數組實作,LinkedList 基于雙向連結清單實作。ArrayList 和 LinkedList 的差別可以歸結為數組和連結清單的差別:

數組支援随機通路,但插入删除的代價很高,需要移動大量元素;

連結清單不支援随機通路,但插入删除隻需要改變指針。

https://www.cnblogs.com/taojietaoge/p/11359542.html

Hashtable 使用 synchronized 來進行同步。

HashMap 可以插入鍵為 null 的 Entry。

HashMap 的疊代器是 fail-fast 疊代器。

HashMap 不能保證随着時間的推移 Map 中的元素次序是不變的。

參考連結:https://www.cnblogs.com/taojietaoge/p/10301711.html

備戰-Java 容器

ConcurrentHashMap 和 HashMap 實作上類似,最主要的差别是 ConcurrentHashMap 采用了分段鎖(Segment),每個分段鎖維護着幾個桶(HashEntry),多個線程可以同時通路不同分段鎖上的桶,進而使其并發度更高(并發度就是 Segment 的個數)。

Segment 繼承自 ReentrantLock。

備戰-Java 容器
備戰-Java 容器

預設的并發級别為 16,也就是說預設建立 16 個 Segment。

每個 Segment 維護了一個 count 變量來統計該 Segment 中的鍵值對個數。

備戰-Java 容器
備戰-Java 容器

在執行 size 操作時,需要周遊所有 Segment 然後把 count 累計起來。

ConcurrentHashMap 在執行 size 操作時先嘗試不加鎖,如果連續兩次不加鎖操作得到的結果一緻,那麼可以認為這個結果是正确的。

嘗試次數使用 RETRIES_BEFORE_LOCK 定義,該值為 2,retries 初始值為 -1,是以嘗試次數為 3。

如果嘗試的次數超過 3 次,就需要對每個 Segment 加鎖。

備戰-Java 容器
備戰-Java 容器

JDK 1.7 使用分段鎖機制來實作并發更新操作,核心類為 Segment,它繼承自重入鎖 ReentrantLock,并發度與 Segment 數量相等。

JDK 1.8 使用了 CAS 操作來支援更高的并發度,在 CAS 操作失敗時使用内置鎖 synchronized。

并且 JDK 1.8 的實作也在連結清單過長時會轉換為紅黑樹。

繼承自 HashMap,是以具有和 HashMap 一樣的快速查找特性。

内部維護了一個雙向連結清單,用來維護插入順序或者 LRU 順序。

備戰-Java 容器
備戰-Java 容器

accessOrder 決定了順序,預設為 false,此時維護的是插入順序。

LinkedHashMap 最重要的是以下用于維護順序的函數,它們會在 put、get 等方法中調用。

當一個節點被通路時,如果 accessOrder 為 true,則會将該節點移到連結清單尾部。也就是說指定為 LRU 順序之後,在每次通路一個節點時,會将這個節點移到連結清單尾部,保證連結清單尾部是最近通路的節點,那麼連結清單首部就是最近最久未使用的節點。 

備戰-Java 容器
備戰-Java 容器

在 put 等操作之後執行,當 removeEldestEntry() 方法傳回 true 時會移除最晚的節點,也就是連結清單首部節點 first。

evict 隻有在建構 Map 的時候才為 false,在這裡為 true。

備戰-Java 容器
備戰-Java 容器

removeEldestEntry() 預設為 false,如果需要讓它為 true,需要繼承 LinkedHashMap 并且覆寫這個方法的實作,這在實作 LRU 的緩存中特别有用,通過移除最近最久未使用的節點,進而保證緩存空間足夠,并且緩存的資料都是熱點資料。

以下是使用 LinkedHashMap 實作的一個 LRU 緩存:

設定最大緩存空間 MAX_ENTRIES 為 3;

使用 LinkedHashMap 的構造函數将 accessOrder 設定為 true,開啟 LRU 順序;

覆寫 removeEldestEntry() 方法實作,在節點多于 MAX_ENTRIES 就會将最近最久未使用的資料移除。

備戰-Java 容器
備戰-Java 容器
備戰-Java 容器
備戰-Java 容器

WeakHashMap 的 Entry 繼承自 WeakReference,被 WeakReference 關聯的對象在下一次垃圾回收時會被回收。

WeakHashMap 主要用來實作緩存,通過使用 WeakHashMap 來引用緩存對象,由 JVM 對這部分緩存進行回收。 

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 來實作緩存功能。

ConcurrentCache 采取的是分代緩存:

經常使用的對象放入 eden 中,eden 使用 ConcurrentHashMap 實作,不用擔心會被回收(伊甸園);

不常用的對象放入 longterm,longterm 使用 WeakHashMap 實作,這些老對象會被垃圾收集器回收。

當調用 get() 方法時,會先從 eden 區擷取,如果沒有找到的話再到 longterm 擷取,當從 longterm 擷取到就把對象放入 eden 中,進而保證經常被通路的節點不容易被回收。

當調用 put() 方法時,如果 eden 的大小超過了 size,那麼就将 eden 中的所有對象都放入 longterm 中,利用虛拟機回收掉一部分不經常使用的對象。

備戰-Java 容器
備戰-Java 容器

玉階生白露

夜久侵羅襪

繼續閱讀