1. 程式人生 > >Java集合類操作優化經驗總結

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

設置 mar ise long 初始化 實際類型 線性表 core 不一定

在實際的項目開發中會有非常多的對象,怎樣高效、方便地管理對象,成為影響程序性能與可維護性的重要環節。

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 時,client代碼不用改變,這就是針對抽象進行編程思想。

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