Java集合類操作優化經驗總結

developerworks發表於2015-08-08

在實際的專案開發中會有很多的物件,如何高效、方便地管理物件,成為影響程式效能與可維護性的重要環節。Java 提供了集合框架來解決此類問題,線性表、連結串列、雜湊表等是常用的資料結構,在進行 Java 開發時,JDK 已經為我們提供了一系列相應的類來實現基本的資料結構,所有類都在 java.util 這個包裡,清單 1 描述了集合類的關係。

清單 1.集合類之間關係
Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap

本文講的就是集合框架的使用經驗總結,注意,本文所有程式碼基於 JDK7。

集合介面

Collection 介面

Collection 是最基本的集合介面,一個 Collection 代表一組 Object,即 Collection 的元素(Elements)。一些 Collection 允許相同的元素、支援對元素進行排序,另一些則不行。JDK 不提供直接繼承自 Collection 的類,JDK 提供的類都是繼承自 Collection 的子介面,如 List 和 Set。所有實現 Collection 介面的類都必須提供兩個標準的建構函式,無引數的建構函式用於建立一個空的 Collection,有一個 Collection 引數的建構函式用於建立一個新的 Collection,這個新的 Collection 與傳入的 Collection 有相同的元素,後一個建構函式允許使用者複製一個 Collection。

如何遍歷 Collection 中的每一個元素?

不論 Collection 的實際型別如何,它都支援一個 iterator() 的方法,該方法返回一個迭代子,使用該迭代子即可逐一訪問 Collection 中每一個元素。典型的用法如下:

Iterator it = collection.iterator(); // 獲得一個迭代子

while(it.hasNext()){

Object obj = it.next(); // 得到下一個元素

}

Collection 介面派生的兩個介面是 List 和 Set。

Collection 介面提供的主要方法:

  1. boolean add(Object o) 新增物件到集合;
  2. boolean remove(Object o) 刪除指定的物件;
  3. int size() 返回當前集合中元素的數量;
  4. boolean contains(Object o) 查詢集合中是否有指定的物件;
  5. boolean isEmpty() 判斷集合是否為空;
  6. Iterator iterator() 返回一個迭代器;
  7. boolean containsAll(Collection c) 查詢集合中是否有集合 C 中的元素;
  8. boolean addAll(Collection c) 將集合 C 中所有的元素新增給該集合;
  9. void clear() 刪除集合中所有元素;
  10. void removeAll(Collection c) 從集合中刪除 C 集合中也有的元素;
  11. void retainAll(Collection c) 從集合中刪除集合 C 中不包含的元素。

List 介面

List 是有序的 Collection,使用此介面能夠精確的控制每個元素插入的位置。使用者能夠使用索引(元素在 List 中的位置,類似於陣列下標)來訪問 List 中的元素,這類似於 Java 的陣列。和下文要提到的 Set 不同,List 允許有相同的元素。

除了具有 Collection 介面必備的 iterator() 方法外,List 還提供一個 listIterator() 方法,返回一個 ListIterator 介面。和標準的 Iterator 介面相比,ListIterator 多了一些 add() 之類的方法,允許新增、刪除、設定元素、向前或向後遍歷等功能。實現 List 介面的常用類有 LinkedList,ArrayList,Vector 和 Stack 等。

List 介面提供的主要方法:

  1. void add(int index,Object element) 在指定位置上新增一個物件;
  2. boolean addAll(int index,Collection c) 將集合 C 的元素新增到指定的位置;
  3. Object get(int index) 返回 List 中指定位置的元素;
  4. int indexOf(Object o) 返回第一個出現元素 O 的位置;
  5. Object removeint(int index) 刪除指定位置的元素;
  6. Object set(int index,Object element) 用元素 element 取代位置 index 上的元素, 返回被取代的元素。

Map 介面

Map 沒有繼承 Collection 介面。Map 提供 Key 到 Value 的對映,一個 Map 中不能包含相同的 Key,每個 Key 只能對映一個 Value。Map 介面提供 3 種集合的檢視,Map 的內容可以被當作一組 Key 集合,一組 Value 集合,或者一組 Key-Value 對映。

Map 提供的主要方法:

  1. boolean equals(Object o) 比較物件;
  2. boolean remove(Object o) 刪除一個物件;
  3. put(Object key,Object value) 新增 key 和 value。

RandomAccess 介面

RandomAccess 介面是一個標誌介面,本身並沒有提供任何方法,任務凡是通過呼叫 RandomAccess 介面的物件都可以認為是支援快速隨機訪問的物件。此介面的主要目的是標識那些可支援快速隨機訪問的 List 實現。任何一個基於陣列的 List 實現都實現了 RaodomAccess 介面,而基於連結串列的實現則都沒有。因為只有陣列能夠進行快速的隨機訪問,而對連結串列的隨機訪問需要進行連結串列的遍歷。因此,此介面的好處是,可以在應用程式中知道正在處理的 List 物件是否可以進行快速隨機訪問,從而針對不同的 List 進行不同的操作,以提高程式的效能。

集合類介紹

LinkedList 類

LinkedList 實現了 List 介面,允許 Null 元素。此外 LinkedList 提供額外的 Get、Remove、Insert 等方法在 LinkedList 的首部或尾部運算元據。這些操作使得 LinkedList 可被用作堆疊(Stack)、佇列(Queue)或雙向佇列(Deque)。請注意 LinkedList 沒有同步方法,它不是執行緒同步的,即如果多個執行緒同時訪問一個 List,則必須自己實現訪問同步。一種解決方法是在建立 List 時構造一個同步的 List,方法如

List list = Collections.synchronizedList(new LinkedList(…));

ArrayList 類

ArrayList 實現了可變大小的陣列。它允許所有元素,包括 Null。Size、IsEmpty、Get、Set 等方法的執行時間為常數,但是 Add 方法開銷為分攤的常數,新增 N 個元素需要 O(N) 的時間,其他的方法執行時間為線性。

每個 ArrayList 例項都有一個容量(Capacity),用於儲存元素的陣列的大小,這個容量可隨著不斷新增新元素而自動增加。當需要插入大量元素時,在插入前可以呼叫 ensureCapacity 方法來增加 ArrayList 的容量以提高插入效率。和 LinkedList 一樣,ArrayList 也是執行緒非同步的(unsynchronized)。

ArrayList 提供的主要方法:

  1. Boolean add(Object o) 將指定元素新增到列表的末尾;
  2. Boolean add(int index,Object element) 在列表中指定位置加入指定元素;
  3. Boolean addAll(Collection c) 將指定集合新增到列表末尾;
  4. Boolean addAll(int index,Collection c) 在列表中指定位置加入指定集合;
  5. Boolean clear() 刪除列表中所有元素;
  6. Boolean clone() 返回該列表例項的一個拷貝;
  7. Boolean contains(Object o) 判斷列表中是否包含元素;
  8. Boolean ensureCapacity(int m) 增加列表的容量,如果必須,該列表能夠容納 m 個元素;
  9. Object get(int index) 返回列表中指定位置的元素;
  10. Int indexOf(Object elem) 在列表中查詢指定元素的下標;
  11. Int size() 返回當前列表的元素個數。

Vector 類

Vector 非常類似於 ArrayList,區別是 Vector 是執行緒同步的。由 Vector 建立的 Iterator,雖然和 ArrayList 建立的 Iterator 是同一介面,但是,因為 Vector 是同步的,當一個 Iterator 被建立而且正在被使用,另一個執行緒改變了 Vector 的狀態(例如,新增或刪除了一些元素),這時呼叫 Iterator 的方法時將丟擲 ConcurrentModificationException,因此必須捕獲該異常。

Stack 類

Stack 繼承自 Vector,實現了一個後進先出的堆疊。Stack 提供 5 個額外的方法使得 Vector 得以被當作堆疊使用。除了基本的 Push 和 Pop 方法,還有 Peek 方法得到棧頂的元素,Empty 方法測試堆疊是否為空,Search 方法檢測一個元素在堆疊中的位置。注意,Stack 剛建立後是空棧。

Set 類

Set 是一種不包含重複的元素的 Collection,即任意的兩個元素 e1 和 e2 都有 e1.equals(e2)=false。Set 最多有一個 null 元素。很明顯,Set 的建構函式有一個約束條件,傳入的 Collection 引數不能包含重複的元素。請注意,必須小心操作可變物件(Mutable Object),如果一個 Set 中的可變元素改變了自身狀態,這可能會導致一些問題。

Hashtable 類

Hashtable 繼承 Map 介面,實現了一個基於 Key-Value 對映的雜湊表。任何非空(non-null)的物件都可作為 Key 或者 Value。新增資料使用 Put(Key,Value),取出資料使用 Get(Key),這兩個基本操作的時間開銷為常數。

Hashtable 通過 Initial Capacity 和 Load Factor 兩個引數調整效能。通常預設的 Load Factor 0.75 較好地實現了時間和空間的均衡。增大 Load Factor 可以節省空間但相應的查詢時間將增大,會影響像 Get 和 Put 這樣的操作。使用 Hashtable 的簡單示例,將 1、2、3 這三個數字放到 Hashtable 裡面,他們的 Key 分別是”one”、”two”、”three”,程式碼如清單 2 所示。

清單 2 .Hashtable 示例
Hashtable numbers = new Hashtable();
numbers.put(“one”, new Integer(1));
numbers.put(“two”, new Integer(2));
numbers.put(“three”, new Integer(3));

如果我們需要取出一個數,比如 2,可以用相應的 key 來取出,程式碼如清單 3 所示。

清單 3.從 Hastable 讀取資料
Integer n = (Integer)numbers.get(“two”); 
System.out.println(“two =”+ n);

由於作為 Key 的物件將通過計算其雜湊函式來確定與之對應的 Value 的位置,因此任何作為 key 的物件都必須實現 HashCode 和 Equals 方法。HashCode 和 Equals 方法繼承自根類 Object,如果你用自定義的類當作 Key 的話,要相當小心,按照雜湊函式的定義,如果兩個物件相同,即 obj1.equals(obj2)=true,則它們的 HashCode 必須相同,但如果兩個物件不同,則它們的 HashCode 不一定不同,如果兩個不同物件的 HashCode 相同,這種現象稱為衝突,衝突會導致操作雜湊表的時間開銷增大,所以儘量定義好的 HashCode() 方法,能加快雜湊表的操作。

如果相同的物件有不同的 HashCode,對雜湊表的操作會出現意想不到的結果(期待的 Get 方法返回 Null),要避免這種問題,最好同時複寫 Equals 方法和 HashCode 方法,而不要只寫其中一個。

HashMap 類

HashMap 和 Hashtable 類似,不同之處在於 HashMap 是執行緒非同步的,並且允許 Null,即 Null Value 和 Null Key。但是將 HashMap 視為 Collection 時(values() 方法可返回 Collection),其迭代子操作時間開銷和 HashMap 的容量成比例。因此,如果迭代操作的效能相當重要的話,不要將 HashMap 的初始化容量設得過高,或者 Load Factor 引數設定過低。

WeakHashMap

WeakHashMap 是一種改進的 HashMap,它對 Key 實行“弱引用”,如果一個 Key 不再被外部所引用,那麼該 Key 可以被 GC 回收。

集合類實踐

ArrayList、Vector、LinkedList 均來自 AbstractList 的實現,而 AbstractList 直接實現了 List 介面,並擴充套件自 AbstarctCollection。ArrayList 和 Vector 使用了陣列實現,ArrayList 沒有對任何一個方法提供執行緒同步,因此不是執行緒安全的,Vector 中絕大部分方法都做了執行緒同步,是一種執行緒安全的實現。LinkedList 使用了迴圈雙向連結串列資料結構,由一系列表項連線而成,一個表項總是包含 3 個部分,元素內容、前驅表項和後驅表項。

當 ArrayList 對容量的需求超過當前陣列的大小時,需要進行擴容。擴容過程中,會進行大量的陣列複製操作,而陣列複製時,最終將呼叫 System.arraycopy() 方法。LinkedList 由於使用了連結串列的結構,因此不需要維護容量的大小,然而每次的元素增加都需要新建一個 Entry 物件,並進行更多的賦值操作,在頻繁的系統呼叫下,對效能會產生一定的影響,在不間斷地生成新的物件還是佔用了一定的資源。而因為陣列的連續性,因此總是在尾端增加元素時,只有在空間不足時才產生陣列擴容和陣列複製。

ArrayList 是基於陣列實現的,而陣列是一塊連續的記憶體空間,如果在陣列的任意位置插入元素,必然導致在該位置後的所有元素需要重新排列,因此其效率較差,儘可能將資料插入到尾部。LinkedList 不會因為插入資料導致效能下降。

ArrayList 的每一次有效的元素刪除操作後都要進行陣列的重組,並且刪除的元素位置越靠前,陣列重組時的開銷越大,要刪除的元素位置越靠後,開銷越小。LinkedList 要移除中間的資料需要便利完半個 List。

清單 4. ArrayList 和 LinkedList 使用程式碼
import java.util.ArrayList;
import java.util.LinkedList;

public class ArrayListandLinkedList {
 public static void main(String[] args){
 long start = System.currentTimeMillis();
 ArrayList list = new ArrayList();
 Object obj = new Object();
 for(int i=0;i<5000000;i++){
 list.add(obj);
 }
 long end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 LinkedList list1 = new LinkedList();
 Object obj1 = new Object();
 for(int i=0;i<5000000;i++){
 list1.add(obj1);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 Object obj2 = new Object();
 for(int i=0;i<1000;i++){
 list.add(0,obj2);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 Object obj3 = new Object();
 for(int i=0;i<1000;i++){
 list1.add(obj1);
 }
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 list.remove(0);
 end = System.currentTimeMillis();
 System.out.println(end-start);

 start = System.currentTimeMillis();
 list1.remove(250000);
 end = System.currentTimeMillis();
 System.out.println(end-start);

 }
}
清單 5. 執行輸出
639
1296
6969
0
0
15

HashMap 是將 Key 做 Hash 演算法,然後將 Hash 值對映到記憶體地址,直接取得 Key 所對應的資料。在 HashMap 中,底層資料結構使用的是陣列,所謂的記憶體地址即陣列的下標索引。HashMap 的高效能需要保證以下幾點:

  1. Hash 演算法必須是高效的;
  2. Hash 值到記憶體地址 (陣列索引) 的演算法是快速的;
  3. 根據記憶體地址 (陣列索引) 可以直接取得對應的值。

HashMap 實際上是一個連結串列的陣列。前面已經介紹過,基於 HashMap 的連結串列方式實現機制,只要 HashCode() 和 Hash() 方法實現得足夠好,能夠儘可能地減少衝突的產生,那麼對 HashMap 的操作幾乎等價於對陣列的隨機訪問操作,具有很好的效能。但是,如果 HashCode() 或者 Hash() 方法實現較差,在大量衝突產生的情況下,HashMap 事實上就退化為幾個連結串列,對 HashMap 的操作等價於遍歷連結串列,此時效能很差。

HashMap 的一個功能缺點是它的無序性,被存入到 HashMap 中的元素,在遍歷 HashMap 時,其輸出是無序的。如果希望元素保持輸入的順序,可以使用 LinkedHashMap 替代。

LinkedHashMap 繼承自 HashMap,具有高效性,同時在 HashMap 的基礎上,又在內部增加了一個連結串列,用以存放元素的順序。

HashMap 通過 hash 演算法可以最快速地進行 Put() 和 Get() 操作。TreeMap 則提供了一種完全不同的 Map 實現。從功能上講,TreeMap 有著比 HashMap 更為強大的功能,它實現了 SortedMap 介面,這意味著它可以對元素進行排序。TreeMap 的效能略微低於 HashMap。如果在開發中需要對元素進行排序,那麼使用 HashMap 便無法實現這種功能,使用 TreeMap 的迭代輸出將會以元素順序進行。LinkedHashMap 是基於元素進入集合的順序或者被訪問的先後順序排序,TreeMap 則是基於元素的固有順序 (由 Comparator 或者 Comparable 確定)。

LinkedHashMap 是根據元素增加或者訪問的先後順序進行排序,而 TreeMap 則根據元素的 Key 進行排序。

清單 6 所示程式碼演示了使用 TreeMap 實現業務邏輯的排序。

清單 6. TreeMap 實現排序
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

public class Student implements Comparable<Student>{

public String name;
public int score;
public Student(String name,int score){
this.name = name;
this.score = score;
}

@Override
//告訴 TreeMap 如何排序
public int compareTo(Student o) {
// TODO Auto-generated method stub
if(o.score<this.score){
return 1;
}else if(o.score>this.score){
return -1;
}
return 0;
}

@Override
public String toString(){
StringBuffer sb = new StringBuffer();
sb.append("name:");
sb.append(name);
sb.append(" ");
sb.append("score:");
sb.append(score);
return sb.toString();
}

public static void main(String[] args){
TreeMap map = new TreeMap();
Student s1 = new Student("1",100);
Student s2 = new Student("2",99);
Student s3 = new Student("3",97);
Student s4 = new Student("4",91);
map.put(s1, new StudentDetailInfo(s1));
map.put(s2, new StudentDetailInfo(s2));
map.put(s3, new StudentDetailInfo(s3));
map.put(s4, new StudentDetailInfo(s4));

//列印分數位於 S4 和 S2 之間的人
Map map1=((TreeMap)map).subMap(s4, s2);
for(Iterator iterator=map1.keySet().iterator();iterator.hasNext();){
Student key = (Student)iterator.next();
System.out.println(key+"->"+map.get(key));
}
System.out.println("subMap end");

//列印分數比 s1 低的人
map1=((TreeMap)map).headMap(s1);
for(Iterator iterator=map1.keySet().iterator();iterator.hasNext();){
Student key = (Student)iterator.next();
System.out.println(key+"->"+map.get(key));
}
System.out.println("subMap end");

//列印分數比 s1 高的人
map1=((TreeMap)map).tailMap(s1);
for(Iterator iterator=map1.keySet().iterator();iterator.hasNext();){
Student key = (Student)iterator.next();
System.out.println(key+"->"+map.get(key));
}
System.out.println("subMap end");
}

}

class StudentDetailInfo{
Student s;
public StudentDetailInfo(Student s){
this.s = s;
}
@Override
public String toString(){
return s.name + "'s detail information";
}
}
清單 7 .執行輸出
name:4 score:91->4's detail information
name:3 score:97->3's detail information
subMap end
name:4 score:91->4's detail information
name:3 score:97->3's detail information
name:2 score:99->2's detail information
subMap end
name:1 score:100->1's detail information
subMap end

WeakHashMap 特點是當除了自身有對 Key 的引用外,如果此 Key 沒有其他引用,那麼此 Map 會自動丟棄該值。如清單 8 所示程式碼宣告瞭兩個 Map 物件,一個是 HashMap,一個是 WeakHashMap,同時向兩個 map 中放入 A、B 兩個物件,當 HashMap 刪除 A,並且 A、B 都指向 Null 時,WeakHashMap 中的 A 將自動被回收掉。出現這個狀況的原因是,對於 A 物件而言,當 HashMap 刪除並且將 A 指向 Null 後,除了 WeakHashMap 中還儲存 A 外已經沒有指向 A 的指標了,所以 WeakHashMap 會自動捨棄掉 a,而對於 B 物件雖然指向了 null,但 HashMap 中還有指向 B 的指標,所以 WeakHashMap 將會保留 B 物件。

清單 8.WeakHashMap 示例程式碼
import java.util.HashMap; 
import java.util.Iterator; 
import java.util.Map; 
import java.util.WeakHashMap; 

public class WeakHashMapTest { 
 public static void main(String[] args) throws Exception { 
 String a = new String("a"); 
 String b = new String("b"); 
 Map weakmap = new WeakHashMap(); 
 Map map = new HashMap(); 
 map.put(a, "aaa"); 
 map.put(b, "bbb");
 weakmap.put(a, "aaa"); 
 weakmap.put(b, "bbb");
 map.remove(a);
 a=null; 
 b=null;
 System.gc(); 
 Iterator i = map.entrySet().iterator(); 
 while (i.hasNext()) { 
 Map.Entry en = (Map.Entry)i.next(); 
 System.out.println("map:"+en.getKey()+":"+en.getValue()); 
 } 
 Iterator j = weakmap.entrySet().iterator(); 
 while (j.hasNext()) { 
 Map.Entry en = (Map.Entry)j.next(); 
 System.out.println("weakmap:"+en.getKey()+":"+en.getValue()); 
 } 
 } 
}
清單 9 .執行輸出
map:b:bbb
weakmap:b:bbb

WeakHashMap 主要通過 expungeStaleEntries 這個函式來實現移除其內部不用的條目,從而達到自動釋放記憶體的目的。基本上只要對 WeakHashMap 的內容進行訪問就會呼叫這個函式,從而達到清除其內部不再為外部引用的條目。但是如果預先生成了 WeakHashMap,而在 GC 以前又不曾訪問該 WeakHashMap, 那不是就不能釋放記憶體了嗎?

清單 10. WeakHashMapTest1
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

public class WeakHashMapTest1 {
 public static void main(String[] args) throws Exception {
 List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
 for (int i = 0; i < 1000; i++) {
 WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
 d.put(new byte[1000][1000], new byte[1000][1000]);
 maps.add(d);
 System.gc();
 System.err.println(i);
 }
 }
}

不改變任何 JVM 引數的情況執行清單 10 所示程式碼,由於 Java 預設記憶體是 64M,丟擲記憶體溢位了錯誤。

清單 11. 執行輸出
241
242
243
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at WeakHashMapTest1.main(WeakHashMapTest1.java:10)

果不其然,WeakHashMap 這個時候並沒有自動幫我們釋放不用的記憶體。清單 12 所示程式碼不會出現記憶體溢位問題。

清單 12. WeakHashMapTest2
import java.util.ArrayList;
import java.util.List;
import java.util.WeakHashMap;

public class WeakHashMapTest2 {
 public static void main(String[] args) throws Exception {
 List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
 for (int i = 0; i < 1000; i++) {
 WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
 d.put(new byte[1000][1000], new byte[1000][1000]);
 maps.add(d);
 System.gc();
 System.err.println(i);
 for (int j = 0; j < i; j++) {
 System.err.println(j + " size" + maps.get(j).size());
 }
 }
 }
}

執行結果發現這次測試輸出正常, 不再出現記憶體溢位問題。

總的來說,WeakHashMap 並不是你什麼也幹它就能自動釋放內部不用的物件的,而是在你訪問它的內容的時候釋放內部不用的物件。

WeakHashMap 實現弱引用,是因為它的 Entry<K,V>是繼承自 WeakReference<K>的,

在 WeakHashMap$Entry<K,V>的類定義及建構函式裡面如清單 13 所示。

清單 13. WeakHashMap 類定義
private static class Entry<K,V> extends WeakReference<K> 
implements Map.Entry<K,V> Entry(K key, V value, ReferenceQueue<K> queue,int hash, Entry<K,V> next) { 
super(key, queue); 
this.value = value; 
this.hash = hash; 
this.next = next; 
}

請注意它構造父類的語句:“super(key, queue);”,傳入的是 Key,因此 Key 才是進行弱引用的,Value 是直接強引用關聯在 this.value 之中。在 System.gc() 時,Key 中的 Byte 陣列進行了回收,而 Value 依然保持 (Value 被強關聯到 Entry 上,Entry 又關聯在 Map 中,Map 關聯在 ArrayList 中)。

For 迴圈中每次都 New 一個新的 WeakHashMap,在 Put 操作後,雖然 GC 將 WeakReference 的 Key 中的 Byte 陣列回收了,並將事件通知到了 ReferenceQueue,但後續卻沒有相應的動作去觸發 WeakHashMap 去處理 ReferenceQueue,所以 WeakReference 包裝 Key 依然存在於 WeakHashMap 中,其對應的 value 也當然存在。

那 value 是何時被清除的呢? 對清單 10 和清單 11 兩個示例程式進行分析可知,清單 11 的 maps.get(j).size() 觸發了 Value 的回收,那又如何觸發的呢?檢視 WeakHashMap 原始碼可知,Size 方法呼叫了 expungeStaleEntries 方法,該方法對 JVM 要回收的的 Entry(Quene 中) 進行遍歷,並將 Entry 的 Value 置空,回收了記憶體。所以效果是 Key 在 GC 的時候被清除,Value 在 Key 清除後訪問 WeakHashMap 被清除。

WeakHashMap 類是執行緒不同步的,可以使用 Collections.synchronizedMap 方法來構造同步的 WeakHashMap, 每個鍵物件間接地儲存為一個弱引用的指示物件。因此,不管是在對映內還是在對映之外,只有在垃圾回收器清除某個鍵的弱引用之後,該鍵才會自動移除。需要注意的是,WeakHashMap 中的值物件由普通的強引用保持。因此應該小心謹慎,確保值物件不會直接或間接地強引用其自身的鍵,因為這會阻止鍵的丟棄。注意,值物件可以通過 WeakHashMap 本身間接引用其對應的鍵,這就是說,某個值物件可能強引用某個其他的鍵物件,而與該鍵物件相關聯的值物件轉而強引用第一個值物件的鍵。

處理此問題的一種方法是,在插入前將值自身包裝在 WeakReferences 中,如:m.put(key, new WeakReference(value)),然後,分別用 get 進行解包,該類所有“collection 檢視方法”返回的迭代器均是快速失敗的,在迭代器建立之後,如果從結構上對對映進行修改,除非通過迭代器自身的 Remove 或 Add 方法,其他任何時間任何方式的修改,迭代器都將丟擲 ConcurrentModificationException。因此,面對併發的修改,迭代器很快就完全失敗,而不是冒著在將來不確定的時間任意發生不確定行為的風險。

注意,我們不能確保迭代器不失敗,一般來說,存在不同步的併發修改時,不可能做出任何完全確定的保證。

總結

綜合前面的介紹和例項程式碼,我們可以知道,如果涉及到堆疊、佇列等操作,應該考慮用 List。對於需要快速插入、刪除元素等操作,應該使用 LinkedList。如果需要快速隨機訪問元素,應該使用 ArrayList。如果程式在單執行緒環境中,或者訪問僅僅在一個執行緒中進行,考慮非同步的類,其效率較高。如果多個執行緒可能同時操作一個類,應該使用同步的類。要特別注意對雜湊表的操作,作為 Key 的物件要正確複寫 Equals 和 HashCode 方法。儘量返回介面而非實際的型別,如返回 List 而非 ArrayList,這樣如果以後需要將 ArrayList 換成 LinkedList 時,客戶端程式碼不用改變,這就是針對抽象進行程式設計思想。

本文只是針對應用層面的分享,後續文章會針對具體原始碼級別的實現進行深入介紹,也會對具體實現所基於的演算法進行深入介紹,請有需要的讀者關注後續文章。

相關文章