天天看點

《徐徐道來話Java》(1):泛型的基本概念

泛型是一種程式設計範式(Programming Paradigm),是為了效率和重用性産生的。由Alexander Stepanov(C++标準庫主要設計師)和David Musser(倫斯勒理工學院CS名譽教授)首次提出,自實作始,就成為了ANSI/ISO C++重要标準之一。

Java自1.5版本開始提供泛型,其本質是一個參數化的類型,那麼,何謂參數化?

參數是一個外部變量。設想一個方法,其參數的名稱和實際的數值是外部傳入的,那麼,該參數的類型是否也作為一個參數,在運作時決定呢?這就是泛型的作用。參考如下代碼:

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

list.add(1);

在第2行,會抛出編譯期錯誤。

The method add(int, String) in the type List<String> is not applicable for the arguments (int)

這就是因為,list在聲明時定義了String為自己需要的類型,而1是一個整型數。在上面的例子中,以下幾種添加方式都是合法的:

list.add("字元串");

list.add(new String());

String str="字元串";

list.add( str);

在J2SE1.5之前的版本中,Java沒有辦法顯式的對容器進行編譯期内容限制,在沒有注釋或者文檔說明的情況下,很容易出現運作時錯誤。下面舉一個錯誤的例子:

ArrayList list = new ArrayList();

list.add(0);

list.add('2');

list.add(3);

//輸出list内容

System.out.println(list);

//周遊輸出list内容

for (int i = 0, len = list.size(); i < len; i++) {

Integer object = (Integer) list.get(i);

System.out.println(object);

}

輸出結果如下所示:

[0, 1, 2, 3]

1

Exception in thread "main" java.lang.ClassCastException: java.lang.Character cannot be cast to java.lang.Integer

at capter3.generic.Generic3_1.test2(Generic3_1.java:28)

at capter3.generic.Generic3_1.main(Generic3_1.java:17)

可以看到,直接輸出list的時候,int類型的1和char類型的2是看不出差別的,假設忽略了這點,直接轉為Integer來使用的時候,就抛出了強制轉化異常。

除了會造成異常外,還可以思考一個問題,如果沒有泛型,那麼list.get(int)方法傳回的始終是個Object,那麼要如何在運作時之外确定它的類型呢?

綜上,已經證明了泛型存在的必要性,它提供了以下能力:

1、避免代碼中的強制類型轉換;

2、限定類型,在編譯時提供一個額外的類型檢查,避免錯誤的值被存入容器;

3、實作一些特别程式設計技巧。比如:提供一個方法用于拷貝對象,在不提供額外方法參數的情況下,使傳回值類型和方法參數類型保持一緻。

根據泛型使用方式的不同,可分為泛型接口、泛型類和泛型方法。它們的定義如下:

1、泛型接口:在接口定義的接口名後加上<泛型參數名>,就定義了一個泛型接口,該泛型參數名的作用域存在于接口定義和整個接口主體之内;

2、泛型類:在類定義的類名後加上<泛型參數名>,就定義了一個泛型類,該泛型參數名的作用域存在于類定義和整個類主體之内;

3、方法類:在方法的傳回值之前加上<泛型參數名>,就定義了一個泛型方法,該泛型參數名的作用域包括方法傳回值,方法參數,方法異常,以及整個方法主體。

下面通過一個例子來分别介紹這幾種泛型的定義方法,示例代碼如下:

/**

 * 在普通的接口後加上<泛型參數名>即可以定義泛型接口

 */

interface GenericInterface<T> {

 * 在類定義後加上<泛型參數名>即可定義一個泛型類,注意後面這個GenericInterface<T>,這裡是使用類的泛型參數,而非定義。

class GenericClass<T> implements GenericInterface<T>{

 * 在傳回值前定義了泛型參數的方法,就是泛型方法。

public <K, E extends Exception> K genericMethod(K param) throws E {

java.util.List<K> list = new ArrayList<K>();

K k = null;

return null;

在上例中,class GenericClass<T> implements GenericInterface<T>中有兩個地方使用了<T>,它們是同一個概念嗎?為了回答這個問題,下面給出幾個基本概念,通過對這些基本概念的掌握,将可以解決大部分類似的泛型問題。

a、類(接口)的泛型定義位置緊接在類(接口)定義之後,可以替代該類(接口)定義内部的任意類型。在該類(接口)被聲明時,确定泛型參數。

b、方法的泛型定義位置在修飾符之後傳回值之前,可以替代該方法中使用的任意類型,包括傳回值、參數以及局部變量。在該方法被調用時,确定泛型參數,一般來說,是通過方法參數來确定的泛型參數。

c、<>的出現有兩種情況,一是定義泛型,二是使用某個類\接口來具象化泛型。

根據上面介紹的幾個基本概念,再來分析class GenericClass<T> implemenets GenericInterface<T>這句代碼。可知,class GenericClass是類的定義,那麼第一個<T>就構成了泛型參數的定義,而接口GenericInterface是定義在别處的,該代碼位置是對此接口的引用,是以,第二個<T>則是使用泛型T來規範GenericInterface。

引申:

如果泛型方法是沒有形參的,那麼是否還有其它方法來指定類型參數?

答案:有方法指定,但是這個文法并不常見,實作代碼如下:

GenericClass<String> gc=new GenericClass<String>();

gc.<String>genericMethod(null);

可以看到這裡出現一個很特别的代碼形式,gc.genericMethod(null)中間多出了一個<String>,這就是為genericMethod方法進行泛型參數定義了。

有界泛型有三個非常重要的關鍵字:?,extends和 super。

a) “?”,表示通配符類型,用于表達任意類型,需要注意的是,它指代的是“某一個任意類型”,但并不是Object;(注意,這裡并不是準确的表達,具體的内容将在“泛型的不變性”相關小節來讨論)

示例代碼如下:

class Parent {

class Sub1 extends Parent {

class Sub2 extends Parent {

class WildcardSample<T> {

T obj;

void test() {

WildcardSample<Parent> sample1 = new WildcardSample<Parent>();

//編譯錯誤

WildcardSample<Parent> sample2 = new WildcardSample<Sub1>();

//正常編譯

WildcardSample<?> sample3 = new WildcardSample<Parent>();

WildcardSample<?> sample4 = new WildcardSample<Sub1>();

WildcardSample<?> sample5 = new WildcardSample<Sub2>();

sample1.obj = new Sub1();

// 編譯錯誤

sample3.obj = new Sub1();

這些代碼展現了通配符的作用。

1、sample2聲明裡使用Parent作為泛型參數的時候,不能指向使用Sub1作為泛型參數的執行個體。因為編譯器處理泛型時嚴格的按照定義來執行,Sub1雖然是Parent的子類,但它畢竟不是Parent。

2、sample3~5聲明裡使用?作為泛型參數的時候,可以指向任意WildcardSample執行個體。

3、sample1.obj可以指向Sub1執行個體,這是因為obj被認為是Parent,而Sub1是Parent的子類,滿足向上轉型。

4、sample3.obj不能指向Sub1執行個體,這是因為通配符是“某個類型”而并不是Object,是以Sub1并不是?的子類,抛出編譯期錯誤。

5、雖然有如此多的限制,但是你還是可以以Object類型來讀取sample3.obj,畢竟不論通配符是什麼類型,Object一定是它的父類。

引申:設想如果sample3.obj = new Sub1()可以編譯通過,事實上期望的sample3類型是WildcardSample<Object>,這樣的話,通配符就失去意義了。而在實際應用中,這并不光是失去意義這樣簡單的事,還會引起執行異常。這裡提供一個例子幫助了解:

      WildcardSample<Parent> sample1 = new WildcardSample<Parent>();

sample1.obj = new Parent();

WildcardSample<?> extSample = sample1;

//原本應當被限定為Parent類型,這裡使用了String類型,必須抛出異常。

extSample.obj = new String();

b) extends在泛型裡不是繼承,而是定義上界的意思,如T extends UpperBound,UpperBound為泛型T的上界,也就是說T必須為UpperBound或者它的子類;

泛型上界可以用于定義以及聲明代碼處,不同的位置使用的時候,它的作用于使用方法都有所不同,示例代碼如下:

 * 有上界的泛型類

class ExtendSample<T extends Parent> {

 * 有上界的泛型方法

<K extends Sub1> T extendMethod(K param) {

return this.obj;

public class Generic3_1_2_b {

public static void main(String[] args) {

ExtendSample<Parent> sample1 = new ExtendSample<Parent>();

ExtendSample<Sub1> sample2 = new ExtendSample<Sub1>();

ExtendSample<? extends Parent> sample3 = new ExtendSample<Sub1>();

ExtendSample<? extends Sub1> sample4;

sample4 = new ExtendSample<Sub2>();

ExtendSample<? extends Number> sample5;

// 編譯錯誤

sample3.obj = new Parent();

這個例子中使用了一個具備上界的泛型方法和一個具備上界的泛型類,它們展現了extends在泛型中的應用:

1、在方法\接口\類的泛型定義時,需要使用泛型參數名(比如T或者K)。

2、在聲明位置使用泛型參數時,需要使用通配符,意義是“用來指定類的上界(該類或其子類)”。

就算加上了上界,使用通配符來定義的對象,也是隻能讀,不能寫。理由在通配符相關小節已經論證過,不再贅述。

c) super關鍵字用于定義泛型的下界。如T super LowerBound,則LowerBound為泛型T的下界,也就是說T必須為LowerBound或者它的父類;

泛型下界隻能應用于聲明代碼處,表示泛型參數一定是指定類或其父類。

參考以下代碼:

class SuperSample<T> {

public class Generic3_1_2_c {

SuperSample<? super Parent> sample1 = new SuperSample<Parent>();

SuperSample<? super Parent> sample2 = new SuperSample<Sub1>();

SuperSample<? super Sub1> sample3 = new SuperSample<Parent>();

sample1.obj = new Sub2();

sample3.obj = new Sub2();

在該示例中,可以注意到:

1、sample1.obj一定是Parent或者Parent的父類,那麼,Sub1\Sub2\Parent都能滿足向上轉型。

2、sample3.obj一定是Sub1或者Sub1的父類,Parent和Sub2無法完全滿足條件,是以抛出了異常。

引申:思考一個問題,在上面的例子裡sample1.obj是什麼類型?

答案: ? extends Parent,也就是說,沒有類型。

通過對上述現象的分析可知:當使用extends上界時,所有以該泛型參數作為形參的方法,都不可用,當使用super下界時,所有以該泛型參數作為傳回值的方法,隻能以Object類型來引用。

思考:<? extends T>和<? super T>有哪些差別?

複雜的泛型也是由簡單的泛型組合起來的,需要掌握下面幾個概念:

1、多個泛型參數定義由逗号隔開,就像<T,K>這樣。

2、同一個泛型參數如果有多個上界,則各個上界之間用&符号連接配接。

3、多個上界類型裡最多隻能有一個類,其他必須為接口,如果上界裡有類,則必須放置在第一位。

結合以上的知識,則可以靈活的組合出複雜的泛型聲明來。參考以下代碼:

class A {

class B extends A {

class C extends B {

 * 這是一個泛型類

class ComplexGeneric<T extends A, K extends B & Serializable & Cloneable>  {...}

通過上面代碼可以看出,ComplextGeneric 類具備兩個泛型參數<T,K>,其中T具備上界A,換言之,T一定是A或者其子類;K具備三個上界,分别為類B,接口 Serializable和Cloneable,換言之,K一定是B或者其子類,并且實作了Serializable和Cloneable。

複雜的泛型為更規範更精确的設計提供了可能性。

引申:前面說過,在運作時,泛型會被處理為上界類型。也就是說,ComplextGeneric在其内部用到泛型T的時候,反射會把它當成A類來處理(需要注意的是,在位元組碼裡,還是當作Object處理),那麼,反射用到泛型K的時候呢?答案是,會把它當成上界定義的第一個上界處理,在目前例子是,也就是B這個類。

知道了這個有什麼意義呢?

設想一個方法 <T extends A> void method(T t);

如果需要反射擷取它,必須同時知道方法名和參數類型。這時候,使用Object是找不到它的,隻能通過A類來擷取。