1. 程式人生 > >JDK框架簡析--java.util包中的工具類庫

JDK框架簡析--java.util包中的工具類庫

題記

JDK,Java Development Kit。

我們必須先認識到,JDK只是,僅僅是一套Java基礎類庫而已,是Sun公司開發的基礎類庫,僅此而已,JDK本身和我們自行書寫總結的類庫,從技術含量來說,還是在一個層級上,它們都是需要被編譯成位元組碼,在JRE中執行的,JDK編譯後的結果就是jre/lib下的rt.jar,我們學習使用它的目的是加深對Java的理解,提高我們的Java編碼水平。

本系列所有文章基於的JDK版本都是1.7.16。

本節內容

在本節中,簡析java.util包所包含的工具類庫,主要是集合相關的類庫,其次還有正則、壓縮解壓、併發、日期時間等工具類。

本篇內容大致、簡單的對於java.util包進行了一個描述,以後會逐漸進行內容補充,本篇文章相當於一個佔位符,所謂先有了骨架,才能逐漸豐滿偷笑

集合類

基本情況

主要介面及其繼承關係如下:

SortedSet  -->  Set --> Collection -->  Iterable

List  -->  Collection  -->  Iterable

SortedMap  -->  Map

常用類及其繼承關係如下:

HashSet/LinkedHashSet  --> Set

TreeSet  -->  SortedSet  --> Set

ArrayList/LinkedList  -->  List

HashMap  -->  Map

TreeMap  -->  SortedMap  -->  Map

統一稱謂:Collection分支的,我們稱之為“聚集”;Map分支的,我們稱之為“對映”。

Collection繼承自Iterable,所以其下的類都可以用迭代器Iterator訪問,也可以用for(E e:es)形式訪問;Map可以用實現了其內部介面Entry的物件,作為一個元素。

Hashtable和HashMap,他們都實現了Map介面;Hashtable繼承自古老的抽象類Dictionary,是執行緒安全的;HashMap繼承自較新的抽象類AbstractMap,不是執行緒安全的。

HashMap允許null的鍵和值,而Hashtable不允許null的鍵和值,這是因為:

Hashtable有方法contains方法(判斷是否存在值),如果允許的話,則不論key或者value為null,都會返回null,這容易誤解,所以Hashtable就強制限制了,對於null 鍵和值,直接丟擲NullPointerException;

HashMap沒有contains方法,分別是containsKey()和containsValues()。

另外JDK5開始,對於執行緒安全的Map,有一種ConcurrentHashMap,高效,其實現執行緒安全的過程中,沒有使用synchronized,是一種分段的結構,並用CAS這種無鎖演算法實現了執行緒安全。

Hash

Object類有兩種方法來推斷物件的標識:equals()和hashCode()。

一般來說,如果您忽略了其中一種,您必須同時忽略這兩種,因為兩者之間有必須維持的至關重要的關係。

特殊情況是根據equals() 方法,如果兩個物件是相等的,它們必須有相同的hashCode()值,Object原始碼中對此有要求,儘管這通常不是真的。

LinkedHashMap,重寫了HashMap的迭代器、AddEntry、Entry等幾個方法和類,用一個雙向連結串列儲存元素加入的順序;這可以按照訪問順序排序,最近訪問的元素(get/put),會被放在連結串列的末尾,這是LRU演算法(Least Recenty Used),最近最少使用演算法。

ArrayList和LinkedList

關於ArrayList和LinkedList,ArrayList是基於陣列的,這種方式將物件放在連續的位置中,讀取快,但是容量不足時需要進行陣列擴容,效能降低,插入和刪除也慢;LinkedList是基於連結串列的,插入和刪除都快,但是查詢麻煩,不能按照索引查詢。所以說,對於構造一個佇列是用ArrayList或者LinkedList,是根據效能和方便來考慮的,比如LinkedList有removeLast(),ArrayList只能remove(index),用LinkedList構造一個Queue的程式碼演示如下:

class Queue {
	private LinkedList<String> llt;
	public Queue() {
		 llt = new LinkedList<String>();
	}
	public void add(String s) {
		llt.add(s);
	}
	public String get() {
		return llt.removeLast();	//佇列
		//return llt.removeFirst();	//堆疊
	}
	public boolean isNull() {
		return llt.isEmpty();
	}
}

ConcurrentModificationException

import java.util.*;
import java.util.Map.Entry;
class Test
{
	public static void main(String[] args) throws Exception {
		HashMap<String,Integer> mTemp = new HashMap<String,Integer>();
		mTemp.put("test1",1);		
		Iterator<Entry<String,Integer>> iTemp = mTemp.entrySet().iterator();
		//以下這行程式碼會引發java.util.ConcurrentModificationException,
		//因為對聚集建立迭代器之後,進行遍歷或者修改操作時,如果遇到期望的修改計數器和實際的修改計數器不一樣的情況(modCount != expectedModCount)
		//就會報這個Exception,樂觀鎖的思想
		//mTemp.put("test2",2);	
		while(iTemp.hasNext()) {
			System.out.println(iTemp.next().getKey());
		}

		//for迴圈,寫法更簡單一些,在編譯後,還是會被轉換為迭代器
		for(Entry<String,Integer> e : mTemp.entrySet()) {
			System.out.println(e.getKey());
		}

		ArrayList<string> al = new ArrayList<string>();
		al.add("test");
		for(String s : al) {
			Integer i = Integer.reverse((new java.util.Random().nextInt(100)));
			al.add(i.toString());	//這行程式碼也會報ConcurrentModificationException		
		}
	}
}

對於這種情況,可以使用java.util.concurrent包中的相關類,比如CopyOnWriteArrayList,就不會報這個異常了,因為CopyOnWriteArrayList類最大的特點就是,在對其例項進行修改操作(add/remove等)會新建一個數據並修改,修改完畢之後,再將原來的引用指向新的陣列。這樣,修改過程沒有修改原來的陣列,也就沒有了ConcurrentModificationException錯誤。

我們可以參考CopyOnWriteArrayList的原始碼:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

ConcurrentModificationException,表明:我正讀取的內容被修改掉了,你是否需要重新遍歷?或是做其它處理?這就是fast-fail(快速失敗機制)的含義。

雖然這只是在一個執行緒之內,並不是多執行緒的,我們同樣也可以這樣理解fast-fail:

Fail-fast是併發中樂觀(optimistic)策略的具體應用,它允許執行緒自由競爭,但在出現衝突的情況下假設你能應對,即你能判斷出問題何在,並且給出解決辦法;

悲觀(pessimistic)策略就正好相反,它總是預先設定足夠的限制,通常是採用鎖(lock),來保證程式進行過程中的無錯,付出的代價是其它執行緒的等待開銷。

快速失敗機制主要目的在於使iterator遍歷陣列的執行緒能及時發現其他執行緒對Map的修改(如put、remove、clear等),因 此,fast-fail並不能保證所有情況下的多執行緒併發錯誤,只能保護iterator遍歷過程中的iterator.next()與寫併發.

TreeSet和Collections.sort

TreeSet是基於TreeMap的實現,底層資料結構是“紅黑樹”,資料加入時已經排好順序,存取及查詢效能不如HashSet;Collections.sort是先把List轉換成陣列,再利用“歸併排序”演算法進行排序,歸併排序是一種穩定排序。

關於TreeMap的文章:

對這兩種排序演算法的效能比較如下(24核、64G記憶體,RHEL6.2):

在資料已經基本排好順序的情況下,排序元素數目,在某個段內(大約是2萬-20萬),TreeSet更高效;其他數目下Collections.sort更高效;

在資料隨機性較強的情況下,排序元素數目,在1萬之內,相差不大,Collections.sort效能略高;在1萬之外,80萬之內,TreeSet效能明顯高於Collections.sort;80萬之外,Collection.sort效能更高;java.util.concurrent.ConcurrentSkipListSet這種基於“跳錶”的執行緒安全的可排序類,在30萬之內,效能高於Collection.sort,30萬之外,效能低於Collection.sort,ConcurrentSkipListSet的排序效能總是低於TreeSet。

ConcurrentSkipListSet有一個平衡的樹形索引機構沒有的好處,就是在併發環境下其表現很好。

這裡可以想象,在沒有了解SkipList這種資料結構之前,如果要在併發環境下構造基於排序的索引結構,那麼也就紅黑樹是一種比較好的選擇了,但是它的平衡操作要求對整個樹形結構的鎖定,因此在併發環境下效能和伸縮性並不好。

程式碼演示如下:

import java.util.TreeSet;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.Arrays;
import java.util.ListIterator;
import java.util.Random;
import java.util.Iterator;

class Test {
        public static void main(String[] args) {

                final int LEN = 300000;

                final int SEED = 100000;
                Random r = new Random();

                System.out.println("---------------------------");

                long b = System.currentTimeMillis();
                TreeSet<Temp> ts = new TreeSet<Temp>(new Comparator<Temp>(){
                        public int compare(Temp t1,Temp t2) {return t1.id-t2.id;}
                });
                for(int i=0;i<LEN;i++) {
                        ts.add(new Temp(r.nextInt(SEED)));
                }

                System.out.println(System.currentTimeMillis() - b);

                ArrayList<Temp> aTemp = new ArrayList<Temp>();
                Iterator<Temp> it = ts.iterator();
                while(it.hasNext()) {
                        aTemp.add(it.next());
                }

                System.out.println(System.currentTimeMillis() - b);

                System.out.println("---------------------------");

                b = System.currentTimeMillis();
                ArrayList<Temp> al = new ArrayList<Temp>();
                for(int i=0;i<LEN;i++) {
                        al.add(new Temp(r.nextInt(SEED)));
                }
                //split to the real excution unit
                /*
                Collections.sort(al,new Comparator<Temp>() {
                        public int compare(Temp t1,Temp t2) {return t1.id-t2.id;}
                });*/
                Temp[] a = new Temp[al.size()];
                al.toArray(a);
                System.out.println(System.currentTimeMillis() - b);
                Arrays.sort(a,new Comparator<Temp>() {
                        public int compare(Temp t1,Temp t2) {return t1.id-t2.id;}
                });
                System.out.println(System.currentTimeMillis() - b);
                ListIterator<Temp> li = al.listIterator();
                for(int i=0;i<a.length;i++) {
                        li.next();
                        li.set(a[i]);
                }
                System.out.println(System.currentTimeMillis() - b);
        }
}

class Temp {
        public Temp(int id) {this.id = id;}
        public int id;
}

一個錯誤驗證:

增減進行過一個錯誤驗證,發現對一個物件使用TreeSet排序,和使用同樣資料Entry<String,Double>進行排序比較,效能很差。開始以為JDK對Entry做過優化,static/final之類,後來把物件也改成final,裡面元素也改成final,發現效能依舊很差,完全不能解釋,感覺無法理解。

後來,發現是兩段程式碼不一致,使用Entry進行排序的程式碼有bug,導致排序的資料很少,所以顯得效能好。。。。

所以,無端的臆測還是不要的,建立在JDK深入理解的基礎上就好。

另外一個排序思路

比如,取出Top 20,也不一定要全部排序,可以只取前20個,經驗證,小資料量時,效能也是非常高,大資料未驗證。程式碼大致如下:

int n = 0;
double minScore = 100;	//Top20中最小的積分
String minKey = "";		//最小值所在的Key
Map<String,Double> skuTop = new HashMap<String,Double>();
Set<String> styles = new HashSet<String>();	//過濾同款

for(String sku :tempSkuViewSkus.get(goodsUser.getKey())) {
	boolean filter = false;
	filter = filterSameStyle(sku,styles);
	if(filter) continue;
	
	//過濾不成功,直接continue
	Set<String> userSet = goodsUserView.get(sku);
	if(userSet == null || userSet.size() == 0) continue;
	//這一步,積分的計算,是最耗時的操作(效能瓶頸所在)
	double score = mathTools.getJaccardSimilar(goodsUser.getValue(), userSet);
	//前20個直接進入Map
	if(n++ < ConstMongo.maxRecomNum) {
		skuTop.put(sku, score);
		if(score < minScore) {
			minScore = score;
			minKey = sku;
		}
		continue;
	}
	if(score <= minScore) continue;
	//替換最小值
	skuTop.remove(minKey);
	skuTop.put(sku, score);
	minScore = score;
	minKey = sku;
	for(Entry<String,Double> e : skuTop.entrySet()) {
		if(e.getValue() < minScore) {
			minScore = e.getValue();
			minKey = e.getKey();
		}
	}
}

正則表示式

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Hello {

	public static void main(String[] args)
	{
		Pattern pattern = Pattern.compile("正則表示式");
		//Pattern pattern = Pattern.compile("Hello,正則表示式\\s[\\S]+");
		Matcher matcher = pattern.matcher("正則表示式 Hello,正則表示式 World");
		//替換第一個符合正則的資料
		System.out.println(matcher.replaceFirst("Java"));
	}
}
常用的開發語言都支援正則表示式,但是其對於正則支援的程度是不一樣的。

Java正則:

併發相關類

壓縮解壓類

其他工具類

定時器、日期、時間、貨幣等