泛型詳解一
泛型的使用
泛型有三種使用方式,分別為:泛型類、泛型介面、泛型方法
4.3 泛型類
泛型型別用於類的定義中,被稱為泛型類。通過泛型可以完成對一組類的操作對外開放相同的介面。最典型的就是各種容器類,如:List、Set、Map。
泛型類的最基本寫法(這麼看可能會有點暈,會在下面的例子中詳解):
class 類名稱 <泛型標識:可以隨便寫任意標識號,標識指定的泛型的型別>{ private 泛型標識 /*(成員變數型別)*/ var; ..... } }
一個最普通的泛型類:
//此處T可以隨便寫為任意標識,常見的如T、E、K、V等形式的引數常用於表示泛型 //在例項化泛型類時,必須指定T的具體型別public class Generic<T>{ //key這個成員變數的型別為T,T的型別由外部指定 private T key; public Generic(T key) { //泛型構造方法形參key的型別也為T,T的型別由外部指定 this.key = key; } public T getKey(){ //泛型方法getKey的返回值型別為T,T的型別由外部指定 return key; } }
//泛型的型別引數只能是類型別(包括自定義類),不能是簡單型別 //傳入的實參型別需與泛型的型別引數型別相同,即為Integer.Generic<Integer> genericInteger = new Generic<Integer>(123456); //傳入的實參型別需與泛型的型別引數型別相同,即為String. Generic<String> genericString = new Generic<String>("key_vlaue"); Log.d("泛型測試","key is " + genericInteger.getKey()); Log.d("泛型測試","key is " + genericString.getKey());
結果
12-27 09:20:04.432 13063-13063/? D/泛型測試: key is 123456 12-27 09:20:04.432 13063-13063/? D/泛型測試: key is key_vlaue
定義的泛型類,就一定要傳入泛型型別實參麼?並不是這樣,在使用泛型的時候如果傳入泛型實參,則會根據傳入的泛型實參做相應的限制,此時泛型才會起到本應起到的限制作用。如果不傳入泛型型別實參的話,在泛型類中使用泛型的方法或成員變數定義的型別可以為任何的型別。
看一個例子:
Generic generic = new Generic("111111"); Generic generic1 = new Generic(4444); Generic generic2 = new Generic(55.55); Generic generic3 = new Generic(false); Log.d("泛型測試","key is " + generic.getKey()); Log.d("泛型測試","key is " + generic1.getKey()); Log.d("泛型測試","key is " + generic2.getKey()); Log.d("泛型測試","key is " + generic3.getKey());
D/泛型測試: key is 111111 D/泛型測試: key is 4444 D/泛型測試: key is 55.55 D/泛型測試: key is false
注意:
- 泛型的型別引數只能是類型別,不能是簡單型別。
- 不能對確切的泛型型別使用instanceof操作。如下面的操作是非法的,編譯時會出錯。
4.4 泛型介面
泛型介面與泛型類的定義及使用基本相同。泛型介面常被用在各種類的生產器中,可以看一個例子: //定義一個泛型介面 public interface Generator<T> { public T next(); } 當實現泛型介面的類,未傳入泛型實參時: 複製程式碼 /** * 未傳入泛型實參時,與泛型類的定義相同,在宣告類的時候,需將泛型的宣告也一起加到類中 * 即:class FruitGenerator<T> implements Generator<T>{ * 如果不宣告泛型,如:class FruitGenerator implements Generator<T>,編譯器會報錯:"Unknown class" */ class FruitGenerator<T> implements Generator<T>{ @Override public T next() { return null; } } 複製程式碼 當實現泛型介面的類,傳入泛型實參時: 複製程式碼 /** * 傳入泛型實參時: * 定義一個生產器實現這個介面,雖然我們只建立了一個泛型介面Generator<T> * 但是我們可以為T傳入無數個實參,形成無數種類型的Generator介面。 * 在實現類實現泛型介面時,如已將泛型型別傳入實參型別,則所有使用泛型的地方都要替換成傳入的實參型別 * 即:Generator<T>,public T next();中的的T都要替換成傳入的String型別。 */ public class FruitGenerator implements Generator<String> { private String[] fruits = new String[]{"Apple", "Banana", "Pear"}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; } }
4.5 泛型萬用字元
我們知道Ingeter
是Number
的一個子類,同時在特性章節中我們也驗證過Generic<Ingeter>
與Generic<Number>
實際上是相同的一種基本型別。那麼問題來了,在使用Generic<Number>
作為形參的方法中,能否使用Generic<Ingeter>
的例項傳入呢?在邏輯上類似於Generic<Number>
和Generic<Ingeter>
是否可以看成具有父子關係的泛型型別呢?
為了弄清楚這個問題,我們使用Generic<T>
這個泛型類繼續看下面的例子:
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
// showKeyValue這個方法編譯器會為我們報錯:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
通過提示資訊我們可以看到Generic<Integer>
不能被看作為`Generic<Number>
的子類。由此可以看出:同一種泛型可以對應多個版本(因為引數型別是不確定的),不同版本的泛型類例項是不相容的。
回到上面的例子,如何解決上面的問題?總不能為了定義一個新的方法來處理Generic<Integer>
型別的類,這顯然與java中的多臺理念相違背。因此我們需要一個在邏輯上可以表示同時是Generic<Integer>
和Generic<Number>
父類的引用型別。由此型別萬用字元應運而生。
我們可以將上面的方法改一下:
public void showKeyValue1(Generic<?> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
型別萬用字元一般是使用?代替具體的型別實參,注意了,此處’?’是型別實參,而不是型別形參。重要說三遍!此處’?’是型別實參,而不是型別形參!此處’?’是型別實參,而不是型別形參!再直白點的意思就是,此處的?和Number、String、Integer一樣都是一種實際的型別,可以把?看成所有型別的父類。是一種真實的型別。
可以解決當具體型別不確定的時候,這個萬用字元就是?;當操作型別時,不需要使用型別的具體功能時,只使用Object類中的功能。那麼可以用 ? 萬用字元來表未知型別。
4.6.1 泛型方法的基本用法
光看上面的例子有的同學可能依然會非常迷糊,我們再通過一個例子,把我泛型方法再總結一下。
public class GenericTest {
//這個類是個泛型類,在上面已經介紹過
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
//我想說的其實是這個,雖然在方法中使用了泛型,但是這並不是一個泛型方法。
//這只是類中一個普通的成員方法,只不過他的返回值是在宣告泛型類已經宣告過的泛型。
//所以在這個方法中才可以繼續使用 T 這個泛型。
public T getKey(){
return key;
}
/**
* 這個方法顯然是有問題的,在編譯器會給我們提示這樣的錯誤資訊"cannot reslove symbol E"
* 因為在類的宣告中並未宣告泛型E,所以在使用E做形參和返回值型別時,編譯器會無法識別。
public E setKey(E key){
this.key = keu
}
*/
}
/**
* 這才是一個真正的泛型方法。
* 首先在public與返回值之間的<T>必不可少,這表明這是一個泛型方法,並且聲明瞭一個泛型T
* 這個T可以出現在這個泛型方法的任意位置.
* 泛型的數量也可以為任意多個
* 如:public <T,K> K showKeyName(Generic<T> container){
* ...
* }
*/
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//當然這個例子舉的不太合適,只是為了說明泛型方法的特性。
T test = container.getKey();
return test;
}
//這也不是一個泛型方法,這就是一個普通的方法,只是使用了Generic<Number>這個泛型類做形參而已。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
//這也不是一個泛型方法,這也是一個普通的方法,只不過使用了泛型萬用字元?
//同時這也印證了泛型萬用字元章節所描述的,?是一種型別實參,可以看做為Number等所有類的父類
public void showKeyValue2(Generic<?> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
/**
* 這個方法是有問題的,編譯器會為我們提示錯誤資訊:"UnKnown class 'E' "
* 雖然我們聲明瞭<T>,也表明了這是一個可以處理泛型的型別的泛型方法。
* 但是隻聲明瞭泛型型別T,並未宣告泛型型別E,因此編譯器並不知道該如何處理E這個型別。
public <T> T showKeyName(Generic<E> container){
...
}
*/
/**
* 這個方法也是有問題的,編譯器會為我們提示錯誤資訊:"UnKnown class 'T' "
* 對於編譯器來說T這個型別並未專案中宣告過,因此編譯也不知道該如何編譯這個類。
* 所以這也不是一個正確的泛型方法宣告。
public void showkey(T genericObj){
}
*/
public static void main(String[] args) {
}
}
4.6.2 類中的泛型方法
當然這並不是泛型方法的全部,泛型方法可以出現雜任何地方和任何場景中使用。但是有一種情況是非常特殊的,當泛型方法出現在泛型類中時,我們再通過一個例子看一下
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型類中聲明瞭一個泛型方法,使用泛型E,這種泛型E可以為任意型別。可以型別與T相同,也可以不同。
//由於泛型方法在宣告的時候會宣告泛型<E>,因此即使在泛型類中並未宣告泛型,編譯器也能夠正確識別泛型方法中識別的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型類中聲明瞭一個泛型方法,使用泛型T,注意這個T是一種全新的型別,可以與泛型類中宣告的T不是同一種類型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子類,所以這裡可以
generateTest.show_1(apple);
//編譯器會報錯,因為泛型型別實參指定的是Fruit,而傳入的實參類是Person
//generateTest.show_1(person);
//使用這兩個方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用這兩個方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
4.6.3 泛型方法與可變引數
再看一個泛型方法和可變引數的例子:
public <T> void printMsg( T... args){
for(T t : args){
Log.d("泛型測試","t is " + t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);
4.6.4 靜態方法與泛型
靜態方法有一種情況需要注意一下,那就是在類中的靜態方法使用泛型:靜態方法無法訪問類上定義的泛型;如果靜態方法操作的引用資料型別不確定的時候,必須要將泛型定義在方法上。
即:如果靜態方法要使用泛型的話,必須將靜態方法也定義成泛型方法。
public class StaticGenerator<T> {
....
....
/**
* 如果在類中定義使用泛型的靜態方法,需要新增額外的泛型宣告(將這個方法定義成泛型方法)
* 即使靜態方法要使用泛型類中已經宣告過的泛型也不可以。
* 如:public static void show(T t){..},此時編譯器會提示錯誤資訊:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
4.6.5 泛型方法總結
泛型方法能使方法獨立於類而產生變化,以下是一個基本的指導原則:
無論何時,如果你能做到,你就該儘量使用泛型方法。也就是說,如果使用泛型方法將整個類泛型化,
那麼就應該使用泛型方法。另外對於一個static的方法而已,無法訪問泛型型別的引數。
所以如果static方法要使用泛型能力,就必須使其成為泛型方法。
4.6 泛型上下邊界
在使用泛型的時候,我們還可以為傳入的泛型型別實參進行上下邊界的限制,如:型別實參只准傳入某種型別的父類或某種型別的子類。
為泛型新增上邊界,即傳入的型別實參必須是指定型別的子型別
public void showKeyValue1(Generic<? extends Number> obj){
Log.d("泛型測試","key value is " + obj.getKey());
}
Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);
//這一行程式碼編譯器會提示錯誤,因為String型別並不是Number型別的子類
//showKeyValue1(generic1);
showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);
如果我們把泛型類的定義也改一下:
public class Generic<T extends Number>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
//這一行程式碼也會報錯,因為String不是Number的子類
Generic<String> generic1 = new Generic<String>("11111");
再來一個泛型方法的例子:
//在泛型方法中新增上下邊界限制的時候,必須在許可權宣告與返回值之間的<T>上新增上下邊界,即在泛型宣告的時候新增
//public <T> T showKeyName(Generic<T extends Number> container),編譯器會報錯:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
T test = container.getKey();
return test;
}
通過上面的兩個例子可以看出:泛型的上下邊界新增,必須與泛型的宣告在一起。
4.7 關於泛型陣列要提一下
看到了很多文章中都會提起泛型陣列,經過檢視sun的說明文件,在java中是”不能建立一個確切的泛型型別的陣列”的。
也就是說下面的這個例子是不可以的:
List<String>[] ls = new ArrayList<String>[10];
而使用萬用字元建立泛型陣列是可以的,如下面這個例子:
List<?>[] ls = new ArrayList<?>[10];
這樣也是可以的:
List<String>[] ls = new ArrayList[10];
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
這種情況下,由於JVM泛型的擦除機制,在執行時JVM是不知道泛型資訊的,所以可以給oa[1]賦上一個ArrayList而不會出現異常,
但是在取出資料的時候卻要做一次型別轉換,所以就會出現ClassCastException,如果可以進行泛型陣列的宣告,
上面說的這種情況在編譯期將不會出現任何的警告和錯誤,只有在執行時才會出錯。
而對泛型陣列的宣告進行限制,對於這樣的情況,可以在編譯期提示程式碼有型別安全問題,比沒有任何提示要強很多。
下面採用萬用字元的方式是被允許的:陣列的型別不可以是型別變數,除非是採用萬用字元的方式,因為對於萬用字元的方式,最後取出資料是要做顯式的型別轉換的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK