這一次,讓我們來好好聊聊Java泛型

本文結合《Effective Java》第五章泛型和自己的理解及實踐,講解了Java泛型的知識點。文章發佈於專欄Effective Java,歡迎讀者訂閱。

你經常這樣寫代碼嗎

1、創建List時,刪掉ide幫你自動生成的尖括號,然後發現編譯器的警告,就按照ide的提示加上註解來消除警告

就像這樣

@SuppressWarnings("unchecked")
Set result = new HashSet(s1);


2、寫一個方法處理String類型的列表,入參是List<String>,然後下一次發現要處理Integer類型的列表,就再寫一個方法,入參改爲List<Integer>


如果你經常寫這樣的代碼,那麼,是時候好好學學Java泛型了。


什麼叫泛型(generic)

聲明中具有一個或者多個類型參數的類或者接口,就是泛型。

直觀來看,我們常用的List,

List<String> list = new ArrayList<>();

尖括號裏可以傳入一個類型參數,它是泛型;

再比如

Map<String,Object> = new HashMap<>()

尖括號裏可以傳入兩個類型參數,它也是泛型;

我們自己定義一個類,public Class MyGeneric<K,V,T>,這也是泛型。


爲什麼要使用泛型

其實,對於每一個泛型,都可以採用原生態類型(raw type)的方法去創建對象,即在定義的時候不帶任何實際類型參數,比如,我們使用原生態類型,創建一個存放郵票的集合

Collection stamps = ... ;

這時候,下面這句代碼是可以編譯通過的

stamps.add(new Coin());//往stamps集合放入了Coin類型的對象

接着,麻煩來了,我們在遍歷該集合時,理所當然的以爲,這個集合裏所有的對象都是Stamp類型

for( Iterator i = stamps.iterator();i.hasNext(); ) {
    Stamp s = (Stamp)i.next();//throws ClassCastException
}

於是,當我們遍歷到那個濫竽充數的coin時,就會強轉失敗,拋出ClassCastException

還好,我們有泛型,我們使用泛型,重新定義stamps

Collection<Stamp> stamps = ... ;

這時候,編譯器就知道這個stamps裏面,只能包含Stamp對象,下面這句代碼,也就會在編譯時就報錯

stamps.add(new Coin());//編譯錯誤

而且在遍歷的時候,我們也就不需要強轉了

for( Iterator i = stamps.iterator();i.hasNext(); ) {
Stamp s = i.next();//無需強轉
}

總結一下,爲什麼要使用泛型?因爲,如果採用原生態類型,就會導致類型安全性問題,同時,在表述性方面也會變差。Java之所以還支持原生態類型,僅僅是出於兼容性考慮。


List list 和 List<Object>的區別

這兩種list,都表示 集合的元素可以是任何對象,那區別在哪呢,三行代碼告訴你差別

List<String> listString = new ArrayList<>();
List listRawType = listString;//編譯通過
List<Object> listObject = listString;//編譯失敗  Type mismatch: cannot convert from List<String> to List<Object>

也就是說,我們可以將List<String>的對象傳遞給List類型的參數,但是不能傳遞給List<Object>;

再往深了說,List<A>永遠不會是List<B>的子類型,泛型List之間不存在父子關係。

這一點和數組是不同的,假如 A extends B,那麼,A[] 就是 B[]的子類型,A[]可以傳遞給B[]類型的參數,但是,這些在List是不存在的。

再舉一個例子,這次引用書中的代碼

    // Uses raw type (List) - fails at runtime! - Page 112
    public static void main(String[] args) {
        List<String> strings = new ArrayList<String>();
        unsafeAdd(strings, new Integer(42));
        String s = strings.get(0); // Compiler-generated cast
    }

    private static void unsafeAdd(List list, Object o) {
        list.add(o);
    }

這段代碼是可以編譯通過的,而在實際運行中,就會導致給List<String>中,插入了一個Integer類型的對象;

修改的方法就是在unsafeAdd方法聲明中用List<Object>代替List,這樣,編譯時就會無法通過。


爲什麼列表優先於數組

因爲數組是運行時類型安全,而列表是編譯時類型安全,可以在編譯時就避免錯誤。

什麼意思?舉個例子,採用數組,下面這段代碼編譯通過

Object[] objecArray = new Long[1];
objecArray[0] = "hey dude";

但是運行時,由於往一個long類型的數組插入字符串的元素,會拋出ArrayStoreException異常

如果使用List

List<Object> objectLIst = new ArrayList<Long>(); //編譯錯誤
objectLIst.add("hey dude");

第一行代碼編譯錯誤,原因和上面提到的一樣,List<Long>不是List<Object>的子類型


泛型方法

這一小節,我們來看看泛型實際運用中的一個例子,泛型方法。

這裏引用書中的例子,下面這個方法,返回兩個集合聯合後的集合

	// Raw type method
	public static Set unionUnSafe(Set s1, Set s2) {
		Set result = new HashSet(s1);
		result.addAll(s2);
		return result;
	}
這個方法很明顯不是類型安全的,調用者可以給方法傳遞元素類型不同的s1和s2,在編譯時會發現很多unchecked告警。 

現在,我們來對這個方法進行泛型化改造

	// Generic method
	public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
		Set<E> result = new HashSet<E>(s1);
		result.addAll(s2);
		return result;
	}
這就是泛型方法,和普通方法不同的地方在於,要在方法修飾符和返回類型之間,聲明類型參數列表

public static  <E>   Set<E> union (Set<E> s1, Set<E> s2)

這樣的方法聲明,通過泛型解決了類型安全的問題,但是有一個侷限性,就是要求輸入的兩個集合的類型必須要相同。當然,我們很容易解決這個問題,比如你可以這樣聲明你的方法

	public static <E extends T,K extends T,T> Set<T> union2(Set<E> s1, Set<K> s2) {
		Set<T> result = new HashSet<T>(s1);
		result.addAll(s2);
		return result;
	}

我們在類型參數列表中,聲明瞭E、K、T三個泛型,其中,E和K都是T的子類,E是s1的類型,K是s2的類型,T是返回值類型。

更優雅的,你還可以使用有限通配符類型來改造這個方法

	public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
		Set<E> result = new HashSet<E>(s1);
		result.addAll(s2);
		return result;
	}
關於有限通配符類型,這是下一小節要講的內容。


有限通配符

有限通配符是爲了解決什麼問題呢?

假設我們定義了下面這個方法,用來處理Number類型的列表

	public static void processNumber(List<Number> numbers) {
		for (Number num : numbers) {
			System.out.println(num);
		}
	}

我們會理所當然的以爲,這個方法也可以處理Integer、Long等 Number子類的元素的集合,

然而,就像我們之前所說的,List<Integer>不是List<Number>的子類,

因此我們的願望落空了,下面這段代碼,連編譯都不通過

	public static void main(String[] args) {
        List<Integer> integers = Arrays.asList(1, 2, 3);
        processNumber(integers);//The method processNumber(List<Number>) in the type GenericDemo1 is not applicable for the arguments (List<Integer>)
    }
這個時候,就要用到有限通配符了

我們要做的,只是把processNumber方法的聲明改爲

public static void processNumber(List<? extends Number> numbers)

這個方法就可以滿足我們處理所以Number類型的List的需求。

既然有extends,那麼自然就會有super

public static void processNumber(List<? super AAA> numbers)

該方法可以處理子元素是AAA父類的集合

關於泛型中extends和super的用法,書中還提到了PECS法則,也即producer-extends,consumer-super,有興趣的同學可以去看看。


不要用通配符類型作爲返回類型

《Effective Java》指出,如果使用通配符類型作爲返回類型,那麼就會強制調用者在調用時使用通配符類型, 而通配符類型對於調用者來說應該是無形的, 通配符類型的作用是使方法能夠接受它們應該接受的參數,拒絕應該拒絕的參數


其他

泛型擦除

簡單說就是,List<String> 在運行時會被擦除爲List


總結

爲什麼要使用泛型?  1、類型安全  2、提高API的通用性   這兩點,分別對應的文章開頭提出的兩個問題。


參考文獻

《Effective Java》第二版






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章