集合類操做優化經驗總結(三)

集合類實踐

ArrayList、Vector、LinkedList 均來自 AbstractList 的實現,而 AbstractList 直接實現了 List 接口,並擴展自 AbstarctCollection。ArrayList 和 Vector 使用了數組實現,ArrayList 沒有對任何一個方法提供線程同步,所以不是線程安全的,Vector 中絕大部分方法都作了線程同步,是一種線程安全的實現。LinkedList 使用了循環雙向鏈表數據結構,由一系列表項鍊接而成,一個表項老是包含 3 個部分,元素內容、前驅表項和後驅表項。java

當 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. 根據內存地址 (數組索引) 能夠直接取得對應的值。app

HashMap 其實是一個鏈表的數組。前面已經介紹過,基於 HashMap 的鏈表方式實現機制,只要 HashCode() 和 Hash() 方法實現得足夠好,可以儘量地減小衝突的產生,那麼對 HashMap 的操做幾乎等價於對數組的隨機訪問操做,具備很好的性能。可是,若是 HashCode() 或者 Hash() 方法實現較差,在大量衝突產生的狀況下,HashMap 事實上就退化爲幾個鏈表,對 HashMap 的操做等價於遍歷鏈表,此時性能不好。ide

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

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

相關文章
相關標籤/搜索