泛型的基本概念
- 泛型: 引數化型別
- 引數:
- 定義方法時有形參
- 呼叫方法時傳遞實參
- 引數化型別: 將型別由原來的具體的型別引數化,類似方法中的變數引數
- 型別定義成引數形式, 可以稱為型別形參
- 在使用或者呼叫時傳入具體的型別,可以稱為型別實參
- 引數:
- 泛型的本質是為了引數化型別
- 在不建立新的型別的情況下,通過泛型指定的不同型別來控制形參具體限制的型別
- 在泛型使用過程中,操作的資料型別被指定為一個引數,這種引數型別可以用在:
- 類 - 泛型類
- 介面 - 泛型介面
- 方法 - 泛型方法
- 泛型示例:
List arrayList = new ArrayList(); arrayList.add("aaaa"); arrayList.add(100); arrayList.forEach(i -> { String item = (String) arrayList.get(i); Log.d("泛型", "item = " + item); });
- 這樣的寫法會導致程式出現異常崩潰結束:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
- 這裡的ArrayList可以存放任意型別,新增了一個String型別,新增了一個Integer型別,再使用時都以String的方式使用,因此程式崩潰
- 泛型就是解決這樣的問題
- 再討論另一種情況,如果將第一行宣告初始的程式碼修改一下,那麼在編譯階段就能發現問題:
List arrayList = new ArrayList<String>(); arrayList.add("aaaa"); arrayList.add(100); // 這一步在編譯階段,編譯器就會報錯 arrayList.forEach(i -> { String item = (String) arrayList.get(i); Log.d("泛型", "item = " + item); });
- 泛型只在編譯階段有效:
List<String> stringArrayList = new ArrayList<String>();
List<Integer> integerArrayList = new ArrayList<Integer>();
Class classStringArrayList = stringArrayList.getClass();
Class classIntegerArrayList = integerArrayList.getClass();
if (classStringArrayList.equals(classIntegerArrayList)) {
Log.d("泛型", "型別相同");
}
可以發現,在編譯過後,程式會採取去泛型化措施.也就是說,Java中的泛型,只在編譯階段有效.在編譯過程中,正確檢驗泛型結果後,會將泛型的相關資訊擦除,並且在物件進入和離開方法的邊界處新增型別檢查和型別轉換方法
- 泛型型別在邏輯上可以看成多個不同的型別,實際上都是相同的基本型別
泛型的使用
- 泛型有三種使用方式:
- 泛型類
- 泛型介面
- 泛型方法
泛型類
- 泛型類: 泛型型別用於類定義中
- 通過泛型類可以完成對一組類的操作對外開發相同的介面
- 最典型的就是各種容器類:
- List
- Set
- Map
- 泛型類的最基本寫法:
class 類名稱 <泛型標識: 標識號,標識指定的泛型的型別> {
private 泛型標識 成員變數型別 成員變數名;
}
- 示例:
/*
* 這裡的T可以為任意標識,通常使用T,E,K,V等形式的參數列示泛型
* 在例項化泛型時,必須指定T的具體型別
*/
public class Generic<T> {
// key這個成員變數的型別為T,T的型別由外部指定
private T key;
// 泛型構造方法形參key的型別也為T,T的型別由外部指定
public Generic(T key) {
this.key = key;
}
// 泛型構造方法getKey的返回值型別為T,T的型別由外部指定
public T getKey() {
}
}
/*
* 泛型的型別引數只可以是類型別,包括自定義類. 不能是簡單型別
*/
// 傳入的實參型別需要與泛型型別的引數型別相同,即Integer
Generic<Integer> genericInteger = new Generic<Integer>(123456);
// 傳入的實參型別需要與泛型型別的引數型別相同,即String
Generic<String> genericString = new Generic<String>("key_value");
Log.d("泛型測試", "key is" + genericInteger.getKey());
Log.d("泛型測試", "key is" + genericString.getKey());
泛型測試: key is 123456
泛型測試: key is key_value
- 泛型類中不一定要傳入泛型型別的實參:
- 如果傳入泛型實參,會根據傳入的泛型實參做相應的限制,此時泛型才會起到本應起到的限制作用
- 如果不傳如泛型型別的實參,在泛型類中使用泛型的方法或者成員變數的定義可以為任何型別
Generic genericString = new Generic("1111"); Generic genericInteger = new Generic(5555); Generic genericBigDecimal = new Generic(66.66); Generic genericBoolean = new Generic(true); Log.d("泛型測試", "key is" + genericString.getKey()); Log.d("泛型測試", "key is" + genericInteger.getKey()); Log.d("泛型測試", "key is" + genericBigDecimal.getKey()); Log.d("泛型測試", "key is" + genericBoolean.getKey());
D/泛型測試: key is 1111 D/泛型測試: key is 5555 D/泛型測試: key is 66.66 D/泛型測試: key is true
- 泛型的型別引數只能是類型別,不能是簡單型別
- 不能對確切的泛型型別使用instanceof操作,編譯時會出錯
泛型介面
- 泛型介面與泛型類的定義及使用基本相同
- 泛型介面常常被用在各種類的生產器中
- 示例:
// 定義一個泛型介面
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)];
}
}
泛型萬用字元
- Integer是number的一個子類 ,Generic< Integer > 與Generic< number > 實際上是相同的一種型別
- 由此,產生如下問題:
- 在使用Generic< number > 作為形參的方法中,能否使用Generic< Integer > 的例項傳入?
- 在邏輯上類似於Generic< number >和Generic< Integer >是否可以看成是具有父子關係的泛型型別呢?
- 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 showKeyValueWildcard(Generic<?> obj) {
Log.d("泛型測試", "key value is" + obj.getKey());
}
- 型別萬用字元一般使用 ? 代替具體的型別實參:
- 此處的 ? 是型別實參, 而不是型別形參.
- 和Number,String,Integer一樣,都是一種實際的型別
- 可以把 ? 看作是所有型別的父類,是一種真實的型別
- 型別萬用字元的使用場景:
- 當具體型別不確定的時候,這個萬用字元就是 ?
- 當操作型別時,不需要使用型別的具體功能,只使用Object類中的功能,那麼可以使用 ? 萬用字元來表示未知的型別
泛型方法
- 泛型類: 在例項化類的時候指明泛型的具體型別
- 泛型方法: 在呼叫方法的時候指明泛型的具體型別
/**
* 泛型方法:
* 1. public 和 返回值中間的 <T> 非常重要,可以理解為宣告此方法為泛型方法
* 2. 只有宣告瞭 <T> 的方法才是泛型方法,泛型類中的使用了泛型的成員方法並不是泛型方法
* 3. <T> 表示該方法將使用泛型型別T,此時才可以在方法中使用泛型型別T
* 4. 與泛型類的定義一樣,此處的T可以為任意標識,常見的比如: T, E, K, V等形式的引數常用於表示泛型
*
* @param tClass 傳入的泛型實參
* @return T 返回值為T型別
*/
public <T> T genericMethod(Class<T> tClass) throws InstanttiationException, IllegalAccessException {
T instance = tClass.newInstance();
return instance;
}
Object obj = genericMethod(Class.forName("com.oxford.test"));
泛型方法的基本用法
- 泛型方法使用示例:
public class GenericTest {
/*
* 下面這個類是一個泛型類
*/
public class Generic<T> {
private T key;
public Generic(T key) {
this.key = key;
}
/*
* 這個方法雖然在方法使用了泛型,但是這不是一個泛型方法
* 這只是類中一個普通的成員方法,只不過返回值是在宣告泛型類已經宣告過的泛型
* 所以在這個方法中才可以繼續使用T這個泛型
*/
public T getKey() {
return key;
}
/*
* 下面的這個方法顯然是有問題的,在編譯器中就會提示錯誤"cannot resolve symbol E"
* 因為在類的宣告中並未宣告泛型E,所以在使用E做形參和返回值型別時,編譯器會無法識別
*
* public E setKey(E key) {
* this.key = key
* }
*/
}
/*
* 下面這個方法是一個泛型方法:
* 首先在public與返回值之間的<T>必不可少,這個表明這是一個泛型方法,並且宣告瞭一個泛型T
* 這個T可以出現在這個泛型方法的任意位置
* 泛型的數量也可以為任意多個
*/
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());
}
/*
* 下面這個方法也不是一個泛型方法
* 這也是一個普通方法,只是使用了泛型萬用字元 ?
* 從這裡可以驗證: 泛型萬用字元 ? 是一種型別實參,可以看作是所有類的父類
*/
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 void main(String[] args) {
}
}
類中的泛型方法
- 泛型方法可以出現在任何地方任何場景中進行使用
- 但是,當泛型方法出現在泛型類中時,情況比較特殊:
public class GenericFruit {
class Fruit {
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit {
@Override
public String toString() {
retrun "apple";
}
}
class Person {
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T> {
public void show_1(T t) {
System.out.println(t.toString());
}
/*
* 在泛型類中宣告一個泛型方法,使用泛型T
* 注意這個T是一種全新的型別,可以與泛型類中宣告的T不是同一個型別
*/
public <T> void show_2(T t) {
System.out.println(t.toString());
}
/*
* 在泛型類中宣告一個泛型方法,使用泛型E. 這種泛型E可以為任意型別,可以與型別T相同
* 由於泛型方法在宣告的時候會宣告泛型 <E>,因此即使在泛型類中並未宣告泛型,編譯器也能夠正確識別泛型方法中識別的泛型
*/
public <E> void show_3(E t) {
System.out.println(t.toString());
}
}
public 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);
}
}
泛型方法與可變引數
- 泛型方法與可變引數:
public <T> void printMsg(T... args) {
for (T t : args) {
Log.d("泛型測試", "t is" + t);
}
}
靜態方法與泛型
- 注意在類中的靜態方法使用泛型:
- 靜態方法無法訪問類上定義的泛型
- 如果靜態方法操作的引用資料型別不確定的時候,必須要將泛型定義在方法上
- 如果靜態方法要使用泛型的話,必須將靜態方法定義成泛型方法:
public class StaticGenerator<T> {
...
...
/*
* 如果在類中定義使用泛型的靜態方法,需要新增額外的泛型宣告 - 將這個方法定義成泛型方法
* 否則會報錯: StaticGenerator cannot be refrenced from static context
*/
public static <T> void show(T t) {
}
}
泛型方法總結
- 泛型方法能使方法獨立於類而產生變化,使用原則:
- 無論何時,如果能做到,就儘量使用泛型方法
- 如果使用泛型方法將整個類泛型話,就應該使用泛型方法
- 對於一個static方法,無法訪問泛型型別的引數.如果static方法要使用泛型,就必須使之成為泛型方法
泛型的上下邊界
- 在使用泛型的時候,可以為傳入的泛型型別實參進行上下邊界的限制:
- 比如: 型別的實參只准傳入某種型別的父類或者某種型別的子類
- 為泛型方法新增上邊界,即傳入的型別實參必須是指定型別的子型別:
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);
*/
showKeyValue2(generic2);
showKeyValue3(generic3);
showKeyValue4(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>("1111");
- 在泛型方法中新增上下邊界限制時,必須在許可權宣告與返回值之間的< T >上新增上下邊界:
/*
* 如果使用:
* public <T> showKeyName(Generic<T extends Number> container);
* 編譯器會報錯.
*/
public <T extends Number> T showKeyName(Generic<T> container) {
System.out.println("container key:" + container.getKey());
T test = container.getKey();
return test;
}
- 從上面可以看出 : 泛型的上下邊界新增,必須與泛型的宣告在一起
泛型陣列
- 在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]; //不允許這樣定義 Object o = lsa; Object[] oa = (Object) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // 不建議這樣使用,但是可以通過執行時檢查 String s = lsa[1].get(0); // 執行時報錯,型別轉換異常
- 由於JVM的擦除機制,在執行時JVM是不知道泛型資訊的:
- 所有可以給oa[1] 賦值一個ArrayList卻不會出現異常
- 但是在取出資料的時候要做一次型別轉換,就會出現ClassCastException
- 如果可以進行泛型陣列的宣告,那麼上面的這種情況在編譯期將不會出現任何警告和錯誤,只有在執行時才會報錯
- 通過對泛型陣列的宣告進行限制,對於這樣的情況,可以在編譯期提示程式碼有型別安全問題
- 陣列的型別不可以是型別變數,除非是採用萬用字元的方式: 因為對於萬用字元的方式,最後取出資料是要做顯式的型別轉換的
List<?>[] lsa= new List<?>[10]; // 可以這樣定義為泛型陣列
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // 可以這樣賦值
Integer i = (Integer) lsa[1].get(0); // 可以這樣取出資料