集合類操作優(yōu)化經(jīng)驗(yàn)總結(jié)

作者:周明耀
原文地址:
http://www.ibm.com/developerworks/cn/java/j-lo-set-operation/index.html

本文首先針對 Java 集合接口進(jìn)行了一些介紹敛劝,并對這些接口的實(shí)現(xiàn)類進(jìn)行詳細(xì)描述,包括 LinkedList、ArrayList蛉幸、Vector邢疙、Stack僻他、Hashtable槐壳、HashMap伊者、WeakHashMap 等坤候,然后對一些實(shí)現(xiàn)類的實(shí)現(xiàn)方式和使用經(jīng)驗(yàn)進(jìn)行講解胁赢,同時重點(diǎn)介紹 WeakHashMap。希望通過本文介紹白筹,可以讓讀者對集合的操作方式智末、注意事項等有一些了解。

在實(shí)際的項目開發(fā)中會有很多的對象徒河,如何高效系馆、方便地管理對象,成為影響程序性能與可維護(hù)性的重要環(huán)節(jié)顽照。Java 提供了集合框架來解決此類問題由蘑,線性表、鏈表代兵、哈希表等是常用的數(shù)據(jù)結(jié)構(gòu)尼酿,在進(jìn)行 Java 開發(fā)時,JDK 已經(jīng)為我們提供了一系列相應(yīng)的類來實(shí)現(xiàn)基本的數(shù)據(jù)結(jié)構(gòu)植影,所有類都在 java.util 這個包里裳擎,清單 1 描述了集合類的關(guān)系。

清單 1.集合類之間關(guān)系

Collection
├List
│├LinkedList
│├ArrayList
│└Vector
│ └Stack
└Set
Map
├Hashtable
├HashMap
└WeakHashMap

本文講的就是集合框架的使用經(jīng)驗(yàn)總結(jié)思币,注意句惯,本文所有代碼基于 JDK7土辩。

集合接口

Collection 接口

Collection 是最基本的集合接口,一個 Collection 代表一組 Object抢野,即 Collection 的元素(Elements)拷淘。一些 Collection 允許相同的元素、支持對元素進(jìn)行排序指孤,另一些則不行启涯。JDK 不提供直接繼承自 Collection 的類,JDK 提供的類都是繼承自 Collection 的子接口恃轩,如 List 和 Set结洼。所有實(shí)現(xiàn) Collection 接口的類都必須提供兩個標(biāo)準(zhǔn)的構(gòu)造函數(shù),無參數(shù)的構(gòu)造函數(shù)用于創(chuàng)建一個空的 Collection叉跛,有一個 Collection 參數(shù)的構(gòu)造函數(shù)用于創(chuàng)建一個新的 Collection松忍,這個新的 Collection 與傳入的 Collection 有相同的元素,后一個構(gòu)造函數(shù)允許用戶復(fù)制一個 Collection筷厘。

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

不論 Collection 的實(shí)際類型如何,它都支持一個 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() 返回當(dāng)前集合中元素的數(shù)量骤铃;
  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 中的位置,類似于數(shù)組下標(biāo))來訪問 List 中的元素硼婿,這類似于 Java 的數(shù)組锌半。和下文要提到的 Set 不同,List 允許有相同的元素寇漫。

除了具有 Collection 接口必備的 iterator() 方法外刊殉,List 還提供一個 listIterator() 方法殉摔,返回一個 ListIterator 接口。和標(biāo)準(zhǔn)的 Iterator 接口相比记焊,ListIterator 多了一些 add() 之類的方法逸月,允許添加、刪除遍膜、設(shè)定元素碗硬、向前或向后遍歷等功能。實(shí)現(xiàn) List 接口的常用類有 LinkedList瓢颅,ArrayList恩尾,VectorStack 等。

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) 返回第一個出現(xiàn)元素 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 的內(nèi)容可以被當(dāng)作一組 Key 集合蒲列,一組 Value 集合窒朋,或者一組 Key-Value 映射。

Map 提供的主要方法:

  1. boolean equals(Object o) 比較對象蝗岖;
  2. boolean remove(Object o) 刪除一個對象侥猩;
  3. put(Object key,Object value) 添加 keyvalue

RandomAccess 接口

RandomAccess 接口是一個標(biāo)志接口抵赢,本身并沒有提供任何方法欺劳,任務(wù)凡是通過調(diào)用 RandomAccess 接口的對象都可以認(rèn)為是支持快速隨機(jī)訪問的對象。此接口的主要目的是標(biāo)識那些可支持快速隨機(jī)訪問的 List 實(shí)現(xiàn)铅鲤。任何一個基于數(shù)組的 List 實(shí)現(xiàn)都實(shí)現(xiàn)了 RaodomAccess 接口划提,而基于鏈表的實(shí)現(xiàn)則都沒有。因?yàn)橹挥袛?shù)組能夠進(jìn)行快速的隨機(jī)訪問邢享,而對鏈表的隨機(jī)訪問需要進(jìn)行鏈表的遍歷鹏往。因此,此接口的好處是骇塘,可以在應(yīng)用程序中知道正在處理的 List 對象是否可以進(jìn)行快速隨機(jī)訪問伊履,從而針對不同的 List 進(jìn)行不同的操作韩容,以提高程序的性能。

集合類介紹

LinkedList 類

LinkedList 實(shí)現(xiàn)了 List 接口唐瀑,允許 Null 元素群凶。此外 LinkedList 提供額外的 Get、Remove介褥、Insert 等方法在 LinkedList 的首部或尾部操作數(shù)據(jù)座掘。這些操作使得 LinkedList 可被用作堆棧(Stack)、隊列(Queue)或雙向隊列(Deque)柔滔。請注意 LinkedList 沒有同步方法溢陪,它不是線程同步的,即如果多個線程同時訪問一個 List睛廊,則必須自己實(shí)現(xiàn)訪問同步形真。一種解決方法是在創(chuàng)建 List 時構(gòu)造一個同步的 List,方法如:

List list = Collections.synchronizedList(new LinkedList(...))超全;

ArrayList 類

ArrayList 實(shí)現(xiàn)了可變大小的數(shù)組咆霜。

它允許所有元素,包括 Null嘶朱。Size蛾坯、IsEmptyGet疏遏、Set 等方法的運(yùn)行時間為常數(shù)脉课,但是 Add 方法開銷為分?jǐn)偟某?shù),添加 N 個元素需要 O(N) 的時間财异,其他的方法運(yùn)行時間為線性倘零。

每個 ArrayList 實(shí)例都有一個容量(Capacity),用于存儲元素的數(shù)組的大小戳寸,這個容量可隨著不斷添加新元素而自動增加呈驶。當(dāng)需要插入大量元素時,在插入前可以調(diào)用 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() 返回該列表實(shí)例的一個拷貝;
  7. Boolean contains(Object o) 判斷列表中是否包含元素;
  8. Boolean ensureCapacity(int m) 增加列表的容量娄昆,如果必須佩微,該列表能夠容納 m 個元素;
  9. Object get(int index) 返回列表中指定位置的元素萌焰;
  10. Int indexOf(Object elem) 在列表中查找指定元素的下標(biāo)哺眯;
  11. Int size() 返回當(dāng)前列表的元素個數(shù)。

Vector 類

Vector 非常類似于 ArrayList扒俯,區(qū)別是 Vector 是線程同步的奶卓。由 Vector 創(chuàng)建的 Iterator,雖然和 ArrayList 創(chuàng)建的 Iterator 是同一接口撼玄,但是夺姑,因?yàn)?Vector 是同步的,當(dāng)一個 Iterator 被創(chuàng)建而且正在被使用掌猛,另一個線程改變了 Vector 的狀態(tài)(例如盏浙,添加或刪除了一些元素),這時調(diào)用 Iterator的方法時將拋出 ConcurrentModificationException荔茬,因此必須捕獲該異常废膘。

Stack 類

Stack 繼承自 Vector,實(shí)現(xiàn)了一個后進(jìn)先出的堆棧慕蔚。Stack 提供 5 個額外的方法使得 Vector 得以被當(dāng)作堆棧使用丐黄。除了基本的 PushPop 方法,還有 Peek 方法得到棧頂?shù)脑兀?code>Empty 方法測試堆棧是否為空孔飒,Search 方法檢測一個元素在堆棧中的位置灌闺。注意,Stack 剛創(chuàng)建后是空棧十偶。

Set 類

Set 是一種不包含重復(fù)的元素的 Collection菩鲜,即任意的兩個元素 e1 和 e2 都有 e1.equals(e2)=false园细。Set 最多有一個 null 元素惦积。很明顯,Set 的構(gòu)造函數(shù)有一個約束條件猛频,傳入的 Collection 參數(shù)不能包含重復(fù)的元素狮崩。請注意,必須小心操作可變對象(Mutable Object)鹿寻,如果一個 Set 中的可變元素改變了自身狀態(tài)睦柴,這可能會導(dǎo)致一些問題。

Hashtable 類

Hashtable 繼承 Map 接口毡熏,實(shí)現(xiàn)了一個基于 Key-Value 映射的哈希表坦敌。任何非空(non-null)的對象都可作為 Key 或者 Value。添加數(shù)據(jù)使用 Put(Key,Value)狱窘,取出數(shù)據(jù)使用 Get(Key)杜顺,這兩個基本操作的時間開銷為常數(shù)。

Hashtable 通過 Initial CapacityLoad Factor 兩個參數(shù)調(diào)整性能蘸炸。通常缺省的 Load Factor 0.75 較好地實(shí)現(xiàn)了時間和空間的均衡躬络。增大 Load Factor 可以節(jié)省空間但相應(yīng)的查找時間將增大,會影響像 Get 和 Put 這樣的操作搭儒。使用 Hashtable 的簡單示例穷当,將 1、2淹禾、3 這三個數(shù)字放到 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));

如果我們需要取出一個數(shù)德撬,比如 2铲咨,可以用相應(yīng)的 key 來取出,代碼如清單 3 所示蜓洪。

清單 3.從 Hastable 讀取數(shù)據(jù):

Integer n = (Integer)numbers.get(“two”); 
System.out.println(“two =”+ n);

由于作為 Key 的對象將通過計算其散列函數(shù)來確定與之對應(yīng)的 Value 的位置纤勒,因此任何作為 key 的對象都必須實(shí)現(xiàn) HashCodeEquals 方法。HashCodeEquals 方法繼承自根類 Object隆檀,如果你用自定義的類當(dāng)作 Key 的話摇天,要相當(dāng)小心,按照散列函數(shù)的定義恐仑,如果兩個對象相同泉坐,即 obj1.equals(obj2)=true,則它們的 HashCode 必須相同裳仆,但如果兩個對象不同腕让,則它們的 HashCode 不一定不同,如果兩個不同對象的 HashCode 相同歧斟,這種現(xiàn)象稱為沖突纯丸,沖突會導(dǎo)致操作哈希表的時間開銷增大,所以盡量定義好的 HashCode() 方法静袖,能加快哈希表的操作觉鼻。

如果相同的對象有不同的 HashCode,對哈希表的操作會出現(xiàn)意想不到的結(jié)果(期待的 Get 方法返回 Null)队橙,要避免這種問題坠陈,最好同時復(fù)寫 Equals 方法和 HashCode 方法萨惑,而不要只寫其中一個。

HashMap 類

HashMap 和 Hashtable 類似仇矾,不同之處在于 HashMap 是線程非同步的咒钟,并且允許 Null,即 Null Value 和 Null Key若未。但是將 HashMap 視為 Collection 時(values() 方法可返回 Collection)朱嘴,其迭代子操作時間開銷和 HashMap 的容量成比例。因此粗合,如果迭代操作的性能相當(dāng)重要的話萍嬉,不要將 HashMap 的初始化容量設(shè)得過高,或者 Load Factor 參數(shù)設(shè)置過低隙疚。

WeakHashMap 類

WeakHashMap 是一種改進(jìn)的 HashMap壤追,它對 Key 實(shí)行“弱引用”,如果一個 Key 不再被外部所引用供屉,那么該 Key 可以被 GC 回收行冰。

集合類實(shí)踐

ArrayList、Vector伶丐、LinkedList 均來自 AbstractList 的實(shí)現(xiàn)悼做,而 AbstractList 直接實(shí)現(xiàn)了 List 接口,并擴(kuò)展自 AbstarctCollection哗魂。ArrayList 和 Vector 使用了數(shù)組實(shí)現(xiàn)肛走,ArrayList 沒有對任何一個方法提供線程同步,因此不是線程安全的录别,Vector 中絕大部分方法都做了線程同步朽色,是一種線程安全的實(shí)現(xiàn)。LinkedList 使用了循環(huán)雙向鏈表數(shù)據(jù)結(jié)構(gòu)组题,由一系列表項連接而成葫男,一個表項總是包含 3 個部分,元素內(nèi)容崔列、前驅(qū)表項和后驅(qū)表項梢褐。

當(dāng) ArrayList 對容量的需求超過當(dāng)前數(shù)組的大小時,需要進(jìn)行擴(kuò)容峻呕。擴(kuò)容過程中利职,會進(jìn)行大量的數(shù)組復(fù)制操作趣效,而數(shù)組復(fù)制時瘦癌,最終將調(diào)用 System.arraycopy() 方法。LinkedList 由于使用了鏈表的結(jié)構(gòu)跷敬,因此不需要維護(hù)容量的大小讯私,然而每次的元素增加都需要新建一個 Entry 對象,并進(jìn)行更多的賦值操作,在頻繁的系統(tǒng)調(diào)用下斤寇,對性能會產(chǎn)生一定的影響桶癣,在不間斷地生成新的對象還是占用了一定的資源。而因?yàn)閿?shù)組的連續(xù)性娘锁,因此總是在尾端增加元素時牙寞,只有在空間不足時才產(chǎn)生數(shù)組擴(kuò)容和數(shù)組復(fù)制。

ArrayList 是基于數(shù)組實(shí)現(xiàn)的莫秆,而數(shù)組是一塊連續(xù)的內(nèi)存空間间雀,如果在數(shù)組的任意位置插入元素,必然導(dǎo)致在該位置后的所有元素需要重新排列镊屎,因此其效率較差惹挟,盡可能將數(shù)據(jù)插入到尾部。LinkedList 不會因?yàn)椴迦霐?shù)據(jù)導(dǎo)致性能下降缝驳。

ArrayList 的每一次有效的元素刪除操作后都要進(jìn)行數(shù)組的重組连锯,并且刪除的元素位置越靠前,數(shù)組重組時的開銷越大用狱,要刪除的元素位置越靠后运怖,開銷越小。LinkedList 要移除中間的數(shù)據(jù)需要便利完半個 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. 運(yùn)行輸出:

639
1296
6969
0
0
15

HashMap 是將 Key 做 Hash 算法驳规,然后將 Hash 值映射到內(nèi)存地址,直接取得 Key 所對應(yīng)的數(shù)據(jù)署海。在 HashMap 中吗购,底層數(shù)據(jù)結(jié)構(gòu)使用的是數(shù)組,所謂的內(nèi)存地址即數(shù)組的下標(biāo)索引砸狞。HashMap 的高性能需要保證以下幾點(diǎn):

  1. Hash 算法必須是高效的捻勉;
  2. Hash 值到內(nèi)存地址 (數(shù)組索引) 的算法是快速的;
  3. 根據(jù)內(nèi)存地址 (數(shù)組索引) 可以直接取得對應(yīng)的值刀森。

HashMap 實(shí)際上是一個鏈表的數(shù)組踱启。前面已經(jīng)介紹過,基于 HashMap 的鏈表方式實(shí)現(xiàn)機(jī)制研底,只要 HashCode()Hash() 方法實(shí)現(xiàn)得足夠好埠偿,能夠盡可能地減少沖突的產(chǎn)生,那么對 HashMap 的操作幾乎等價于對數(shù)組的隨機(jī)訪問操作榜晦,具有很好的性能冠蒋。但是,如果 HashCode() 或者 Hash() 方法實(shí)現(xiàn)較差乾胶,在大量沖突產(chǎn)生的情況下抖剿,HashMap 事實(shí)上就退化為幾個鏈表朽寞,對 HashMap 的操作等價于遍歷鏈表,此時性能很差斩郎。

HashMap 的一個功能缺點(diǎn)是它的無序性脑融,被存入到 HashMap 中的元素,在遍歷 HashMap 時缩宜,其輸出是無序的肘迎。如果希望元素保持輸入的順序,可以使用 LinkedHashMap 替代锻煌。

LinkedHashMap 繼承自 HashMap膜宋,具有高效性,同時在 HashMap 的基礎(chǔ)上炼幔,又在內(nèi)部增加了一個鏈表秋茫,用以存放元素的順序。

HashMap 通過 hash 算法可以最快速地進(jìn)行 Put()Get() 操作乃秀。TreeMap 則提供了一種完全不同的 Map 實(shí)現(xiàn)肛著。從功能上講,TreeMap 有著比 HashMap 更為強(qiáng)大的功能跺讯,它實(shí)現(xiàn)了 SortedMap 接口枢贿,這意味著它可以對元素進(jìn)行排序。TreeMap 的性能略微低于 HashMap刀脏。如果在開發(fā)中需要對元素進(jìn)行排序局荚,那么使用 HashMap 便無法實(shí)現(xiàn)這種功能,使用 TreeMap 的迭代輸出將會以元素順序進(jìn)行愈污。LinkedHashMap 是基于元素進(jìn)入集合的順序或者被訪問的先后順序排序耀态,TreeMap 則是基于元素的固有順序 (由 Comparator 或者 Comparable 確定)。

LinkedHashMap 是根據(jù)元素增加或者訪問的先后順序進(jìn)行排序暂雹,而 TreeMap 則根據(jù)元素的 Key 進(jìn)行排序首装。

清單 6 所示代碼演示了使用 TreeMap 實(shí)現(xiàn)業(yè)務(wù)邏輯的排序。

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));

//打印分?jǐn)?shù)位于 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");

//打印分?jǐn)?shù)比 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");

//打印分?jǐn)?shù)比 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 .運(yùn)行輸出:

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 特點(diǎn)是當(dāng)除了自身有對 Key 的引用外杭跪,如果此 Key 沒有其他引用仙逻,那么此 Map 會自動丟棄該值。如清單 8 所示代碼聲明了兩個 Map 對象涧尿,一個是 HashMap系奉,一個是 WeakHashMap,同時向兩個 map 中放入 A姑廉、B 兩個對象缺亮,當(dāng) HashMap 刪除 A,并且 A庄蹋、B 都指向 Null 時瞬内,WeakHashMap 中的 A 將自動被回收掉癌佩。出現(xiàn)這個狀況的原因是需忿,對于 A 對象而言器钟,當(dāng) HashMap 刪除并且將 A 指向 Null 后际度,除了 WeakHashMap 中還保存 A 外已經(jīng)沒有指向 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 .運(yùn)行輸出:

map:b:bbb
weakmap:b:bbb

WeakHashMap 主要通過 expungeStaleEntries 這個函數(shù)來實(shí)現(xiàn)移除其內(nèi)部不用的條目粉铐,從而達(dá)到自動釋放內(nèi)存的目的÷钡担基本上只要對 WeakHashMap 的內(nèi)容進(jìn)行訪問就會調(diào)用這個函數(shù)蝙泼,從而達(dá)到清除其內(nèi)部不再為外部引用的條目。但是如果預(yù)先生成了 WeakHashMap劝枣,而在 GC 以前又不曾訪問該 WeakHashMap, 那不是就不能釋放內(nèi)存了嗎汤踏?

清單 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);
 }
 }
}

不改變?nèi)魏?JVM 參數(shù)的情況運(yùn)行清單 10 所示代碼,由于 Java 默認(rèn)內(nèi)存是 64M舔腾,拋出內(nèi)存溢出了錯誤溪胶。

清單 11. 運(yùn)行輸出:

241
242
243
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at WeakHashMapTest1.main(WeakHashMapTest1.java:10)

果不其然,WeakHashMap 這個時候并沒有自動幫我們釋放不用的內(nèi)存稳诚。清單 12 所示代碼不會出現(xiàn)內(nèi)存溢出問題哗脖。

清單 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());
 }
 }
 }
}

運(yùn)行結(jié)果發(fā)現(xiàn)這次測試輸出正常, 不再出現(xiàn)內(nèi)存溢出問題。

總的來說扳还,WeakHashMap 并不是你什么也干它就能自動釋放內(nèi)部不用的對象的才避,而是在你訪問它的內(nèi)容的時候釋放內(nèi)部不用的對象。

WeakHashMap 實(shí)現(xiàn)弱引用氨距,是因?yàn)樗?Entry<K,V>是繼承自 WeakReference<K>的工扎,
WeakHashMap$Entry<K,V>的類定義及構(gòu)造函數(shù)里面如清單 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; 
}

請注意它構(gòu)造父類的語句:“super(key, queue);”衔蹲,傳入的是 Key肢娘,因此 Key 才是進(jìn)行弱引用的,Value 是直接強(qiáng)引用關(guān)聯(lián)在 this.value 之中舆驶。在 System.gc() 時橱健,Key 中的 Byte 數(shù)組進(jìn)行了回收,而 Value 依然保持 (Value 被強(qiáng)關(guān)聯(lián)到 Entry 上沙廉,Entry 又關(guān)聯(lián)在 Map 中拘荡,Map 關(guān)聯(lián)在 ArrayList 中)。

For 循環(huán)中每次都 New 一個新的 WeakHashMap撬陵,在 Put 操作后珊皿,雖然 GC 將 WeakReference 的 Key 中的 Byte 數(shù)組回收了网缝,并將事件通知到了 ReferenceQueue,但后續(xù)卻沒有相應(yīng)的動作去觸發(fā) WeakHashMap 去處理 ReferenceQueue蟋定,所以 WeakReference 包裝 Key 依然存在于 WeakHashMap 中粉臊,其對應(yīng)的 value 也當(dāng)然存在。

那 value 是何時被清除的呢? 對清單 10 和清單 11 兩個示例程序進(jìn)行分析可知驶兜,清單 11 的 maps.get(j).size() 觸發(fā)了 Value 的回收扼仲,那又如何觸發(fā)的呢?查看 WeakHashMap 源碼可知,Size 方法調(diào)用了 expungeStaleEntries 方法抄淑,該方法對 JVM 要回收的的 Entry(Quene 中) 進(jìn)行遍歷屠凶,并將 Entry 的 Value 置空,回收了內(nèi)存肆资。所以效果是 Key 在 GC 的時候被清除矗愧,Value 在 Key 清除后訪問 WeakHashMap 被清除。

WeakHashMap 類是線程不同步的郑原,可以使用 Collections.synchronizedMap 方法來構(gòu)造同步的 WeakHashMap, 每個鍵對象間接地存儲為一個弱引用的指示對象唉韭。因此,不管是在映射內(nèi)還是在映射之外颤专,只有在垃圾回收器清除某個鍵的弱引用之后纽哥,該鍵才會自動移除。需要注意的是栖秕,WeakHashMap 中的值對象由普通的強(qiáng)引用保持春塌。因此應(yīng)該小心謹(jǐn)慎,確保值對象不會直接或間接地強(qiáng)引用其自身的鍵簇捍,因?yàn)檫@會阻止鍵的丟棄只壳。注意,值對象可以通過 WeakHashMap 本身間接引用其對應(yīng)的鍵暑塑,這就是說吼句,某個值對象可能強(qiáng)引用某個其他的鍵對象,而與該鍵對象相關(guān)聯(lián)的值對象轉(zhuǎn)而強(qiáng)引用第一個值對象的鍵事格。

處理此問題的一種方法是惕艳,在插入前將值自身包裝在 WeakReferences 中,如:m.put(key, new WeakReference(value))驹愚,然后远搪,分別用 get 進(jìn)行解包,該類所有“collection 視圖方法”返回的迭代器均是快速失敗的逢捺,在迭代器創(chuàng)建之后谁鳍,如果從結(jié)構(gòu)上對映射進(jìn)行修改,除非通過迭代器自身的 RemoveAdd 方法,其他任何時間任何方式的修改倘潜,迭代器都將拋出 ConcurrentModificationException绷柒。因此,面對并發(fā)的修改涮因,迭代器很快就完全失敗废睦,而不是冒著在將來不確定的時間任意發(fā)生不確定行為的風(fēng)險。

注意蕊退,我們不能確保迭代器不失敗郊楣,一般來說憔恳,存在不同步的并發(fā)修改時瓤荔,不可能做出任何完全確定的保證。

總結(jié)

綜合前面的介紹和實(shí)例代碼钥组,我們可以知道输硝,如果涉及到堆棧、隊列等操作程梦,應(yīng)該考慮用 List点把。對于需要快速插入、刪除元素等操作屿附,應(yīng)該使用 LinkedList郎逃。如果需要快速隨機(jī)訪問元素,應(yīng)該使用 ArrayList挺份。如果程序在單線程環(huán)境中褒翰,或者訪問僅僅在一個線程中進(jìn)行,考慮非同步的類匀泊,其效率較高优训。如果多個線程可能同時操作一個類,應(yīng)該使用同步的類各聘。要特別注意對哈希表的操作揣非,作為 Key 的對象要正確復(fù)寫 EqualsHashCode 方法。盡量返回接口而非實(shí)際的類型躲因,如返回 List 而非 ArrayList早敬,這樣如果以后需要將 ArrayList 換成 LinkedList 時,客戶端代碼不用改變大脉,這就是針對抽象進(jìn)行編程思想搞监。

本文只是針對應(yīng)用層面的分享,后續(xù)文章會針對具體源代碼級別的實(shí)現(xiàn)進(jìn)行深入介紹箱靴,也會對具體實(shí)現(xiàn)所基于的算法進(jìn)行深入介紹腺逛,請有需要的讀者關(guān)注后續(xù)文章。

參考資料

  • 參考書籍《Java 程序優(yōu)化》,了解關(guān)于集合類的優(yōu)化方法棍矛。
  • 參考書籍《Effective Java》安疗,這本書是 Java 優(yōu)化知識的開山之作,建議讀英文版够委。
  • developerWorks Java 技術(shù)專區(qū):這里有數(shù)百篇關(guān)于 Java 編程各個方面的文章荐类。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市茁帽,隨后出現(xiàn)的幾起案子玉罐,更是在濱河造成了極大的恐慌,老刑警劉巖潘拨,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吊输,死亡現(xiàn)場離奇詭異,居然都是意外死亡铁追,警方通過查閱死者的電腦和手機(jī)季蚂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琅束,“玉大人扭屁,你說我怎么就攤上這事∩鳎” “怎么了料滥?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長艾船。 經(jīng)常有香客問我葵腹,道長,這世上最難降的妖魔是什么丽声? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任礁蔗,我火速辦了婚禮,結(jié)果婚禮上雁社,老公的妹妹穿的比我還像新娘浴井。我一直安慰自己,他們只是感情好霉撵,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布磺浙。 她就那樣靜靜地躺著,像睡著了一般徒坡。 火紅的嫁衣襯著肌膚如雪撕氧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天喇完,我揣著相機(jī)與錄音伦泥,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛不脯,可吹牛的內(nèi)容都是我干的府怯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼防楷,長吁一口氣:“原來是場噩夢啊……” “哼牺丙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起复局,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤冲簿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后亿昏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體峦剔,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年龙优,在試婚紗的時候發(fā)現(xiàn)自己被綠了羊异。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片事秀。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡彤断,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出易迹,到底是詐尸還是另有隱情宰衙,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布睹欲,位于F島的核電站供炼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏窘疮。R本人自食惡果不足惜袋哼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闸衫。 院中可真熱鬧涛贯,春花似錦、人聲如沸蔚出。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骄酗。三九已至稀余,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間趋翻,已是汗流浹背睛琳。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人师骗。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓茁影,卻偏偏與公主長得像,于是被迫代替她去往敵國和親丧凤。 傳聞我的和親對象是個殘疾皇子募闲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容

  • title: java集合框架學(xué)習(xí)總結(jié) tags:集合框架 categories:總結(jié) date: 2017-03...
    行徑行閱讀 1,685評論 0 2
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法愿待,內(nèi)部類的語法浩螺,繼承相關(guān)的語法,異常的語法仍侥,線程的語...
    子非魚_t_閱讀 31,623評論 18 399
  • 里河和淮江是同一條河的兩股支流要出,兩股支流之間只有唯一一條相通的河流,叫做里淮澗农渊。里淮澗正如名里“澗”字的含義患蹂,夾...
    lk洛閱讀 2,320評論 2 4
  • 6月8日零時首映传于,截至6月10日晚20點(diǎn)50分,票房已經(jīng)破8億醉顽!這就是電影《魔獸》的創(chuàng)世紀(jì)沼溜。看著這樣的戰(zhàn)績游添,恐怕很...
    袖卷千重雪閱讀 555評論 2 4
  • 三月風(fēng)光多曼妙 碧波楊柳云中繞 誰憎寒冬風(fēng)雪橋 自古落花付流水 怎奈紅冰印成灰 異地他鄉(xiāng)獨(dú)徘徊
    滴墨無痕閱讀 117評論 0 0