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


泛型是一種編程范式(Programming Paradigm,是為了效率和重用性產生的。由Alexander StepanovC++標准庫主要設計師)和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(1);

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]

0

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> {

T obj;

/**

 * 有上界的泛型方法

 */

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

 

sample1.obj = new Sub1();

 

// 編譯錯誤

sample3.obj = new Parent();

}

}

 

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

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

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

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

 

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

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

參考以下代碼:

class SuperSample<T> {

T obj;

}

 

public class Generic3_1_2_c {

public static void main(String[] args) {

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

// 編譯錯誤

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

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

 

sample1.obj = new Sub1();

sample1.obj = new Sub2();

sample1.obj = new Parent();

 

sample3.obj = new Sub1();

// 編譯錯誤

sample3.obj = new Sub2();

// 編譯錯誤

sample3.obj = new Parent();

 

}

}

 

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

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類來獲取。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM