Java核心基礎知識-泛型
泛型
# Java 泛型瞭解麼?什麼是型別擦除?介紹一下常用的萬用字元?
Java 泛型(generics) 是 JDK 5 中引入的一個新特性, 泛型提供了編譯時型別安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的型別。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。
Java 的泛型是偽泛型,這是因為 Java 在執行期間,所有的泛型資訊都會被擦掉,這也就是通常所說型別擦除 。
List<Integer> list = new ArrayList<>(); list.add(12); //這裡直接新增會報錯 list.add("a"); Class<? extends List> clazz = list.getClass(); Method add = clazz.getDeclaredMethod("add", Object.class); //但是通過反射新增是可以的 //這就說明在執行期間所有的泛型資訊都會被擦掉 // 這說明了Integer泛型例項在編譯之後被擦除掉了,只保留了原始型別,原始型別也變為Object。 add.invoke(list, "kl"); System.out.println(list); ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); // true,都是原始型別:list,說明泛型型別String和Integer都被擦除掉了,只剩下原始型別。
Java泛型的實現方法:型別擦除
Java的泛型基本上都是在編譯器這個層次上實現的,在生成的位元組碼中是不包含泛型中的型別資訊的,使用泛型的時候加上型別引數,在編譯器編譯的時候會去掉,這個過程成為型別擦除。
原始型別 就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別,無論何時定義一個泛型,相應的原始型別都會被自動提供,型別變數擦除,並使用其限定型別(無限定的變數用Object)替換。在泛型類被型別擦除的時候,之前泛型類中的型別引數部分如果沒有指定上限,如 <T>
則會被轉譯成普通的 Object 型別,如果指定了上限如 <T extends String>
則型別引數就被替換成型別上限
如果型別變數有限定,那麼原始型別就用第一個邊界的型別變數類替換。
比如: Pair這樣宣告的話
public class Pair<T extends Comparable> {}
那麼原始型別就是Comparable
。
在呼叫泛型方法時,可以指定泛型,也可以不指定泛型。
- 在不指定泛型的情況下,泛型變數的型別為該方法中的幾種型別的同一父類的最小級,直到Object
- 在指定泛型的情況下,該方法的幾種型別必須是該泛型的例項的型別或者其子類
public class Test {
public static void main(String[] args) {
/**不指定泛型的時候*/
int i = Test.add(1, 2); //這兩個引數都是Integer,所以T為Integer型別
Number f = Test.add(1, 1.2); //這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
Object o = Test.add(1, "asd"); //這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
/**指定泛型的時候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer型別或者其子類
int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float
Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候的泛型為Object
,就比如ArrayList
中,如果不指定泛型,那麼這個ArrayList
可以儲存任意的物件。
疑問
Q: ** 既然說型別變數會在編譯的時候擦除掉,那為什麼我們往 ArrayList 建立的物件中新增整數會報錯**呢?不是說泛型變數String會在編譯的時候變為Object型別嗎?為什麼不能存別的型別呢?既然型別擦除了,如何保證我們只能使用泛型變數限定的型別呢?
A: Java編譯器是通過先檢查程式碼中泛型的型別,然後在進行型別擦除,再進行編譯。
這個型別檢查是針對誰的呢?
以 ArrayList舉例子,以前的寫法:
ArrayList list = new ArrayList();
現在的寫法:
ArrayList<String> list = new ArrayList<String>();
如果是與以前的程式碼相容,各種引用傳值之間,必然會出現如下的情況:
ArrayList<String> list1 = new ArrayList(); //第一種 情況
ArrayList list2 = new ArrayList<String>(); //第二種 情況
這樣是沒有錯誤的,不過會有個編譯時警告。
不過在第一種情況,可以實現與完全使用泛型引數一樣的效果,第二種則沒有效果。
因為型別檢查就是編譯時完成的,new ArrayList()
只是在記憶體中開闢了一個儲存空間,可以儲存任何型別物件,而真正設計型別檢查的是它的引用,因為我們是使用它引用list1
來呼叫它的方法,比如說呼叫add
方法,所以list1
引用能完成泛型型別的檢查。而引用list2
沒有使用泛型,所以不行。
舉例子:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //編譯通過
list1.add(1); //編譯錯誤
String str1 = list1.get(0); //返回型別就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //編譯通過
list2.add(1); //編譯通過
Object object = list2.get(0); //返回型別就是Object
new ArrayList<String>().add("11"); //編譯通過
new ArrayList<String>().add(22); //編譯錯誤
String str2 = new ArrayList<String>().get(0); //返回型別就是String
}
}
通過上面的例子,我們可以明白,型別檢查就是針對引用的,誰是一個引用,用這個引用呼叫泛型方法,就會對這個引用呼叫的方法進行型別檢測,而無關它真正引用的物件。
泛型中引數化型別為什麼不考慮繼承關係?
在Java中,像下面形式的引用傳遞是不允許的:
ArrayList<String> list1 = new ArrayList<Object>(); //編譯錯誤
ArrayList<Object> list2 = new ArrayList<String>(); //編譯錯誤
我們先看第一種情況,將第一種情況拓展成下面的形式:
ArrayList<Object> list1 = new ArrayList<Object>();
list1.add(new Object());
list1.add(new Object());
ArrayList<String> list2 = list1; //編譯錯誤
實際上,在第4行程式碼的時候,就會有編譯錯誤。那麼,我們先假設它編譯沒錯。那麼當我們使用list2
引用用get()
方法取值的時候,返回的都是String
型別的物件(上面提到了,型別檢測是根據引用來決定的),可是它裡面實際上已經被我們存放了Object
型別的物件,這樣就會有ClassCastException
了。所以為了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。(這也是泛型出現的原因,就是為了解決型別轉換的問題,我們不能違背它的初衷)。
再看第二種情況,將第二種情況拓展成下面的形式:
ArrayList<String> list1 = new ArrayList<String>();
list1.add(new String());
list1.add(new String());
ArrayList<Object> list2 = list1; //編譯錯誤
沒錯,這樣的情況比第一種情況好的多,最起碼,在我們用list2
取值的時候不會出現ClassCastException
,因為是從String
轉換為Object
。可是,這樣做有什麼意義呢,泛型出現的原因,就是為了解決型別轉換的問題。我們使用了泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。所以java不允許這麼幹。再說,你如果又用list2
往裡面add()
新的物件,那麼到時候取得時候,我怎麼知道我取出來的到底是String
型別的,還是Object
型別的呢?
Q: 型別擦除的問題,所以所有的泛型型別變數最後都會被替換為原始型別。既然都被替換為原始型別,那麼為什麼我們在獲取的時候,不需要進行強制型別轉換呢?
看下ArrayList.get()
方法:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
可以看到,在return
之前,會根據泛型變數進行強轉。假設泛型型別變數為Date
,雖然泛型資訊會被擦除掉,但是會將(E) elementData[index]
,編譯為(Date)elementData[index]
。所以我們不用自己進行強轉。當存取一個泛型域時也會自動插入強制型別轉換。假設Pair
類的value
域是public
的,那麼表示式:
Date date = pair.value;
也會自動地在結果位元組碼中插入強制型別轉換。
泛型型別變數不能是基本資料型別
不能用型別引數替換基本型別。就比如,沒有ArrayList<double>
,只有ArrayList<Double>
。因為當型別擦除後,ArrayList
的原始型別變為Object
,但是Object
型別不能儲存double
值,只能引用Double
的值。
泛型在靜態方法和靜態類中的問題
泛型類中的靜態方法和靜態變數不可以使用泛型類所宣告的泛型型別引數
舉例說明:
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
}
因為泛型類中的泛型引數的例項化是在定義物件的時候指定的,而靜態變數和靜態方法不需要使用物件來呼叫。物件都沒有建立,如何確定這個泛型引數是何種型別,所以當然是錯誤的。
但是要注意區分下面的一種情況:
public class Test2<T> {
public static <T >T show(T one){ //這是正確的
return null;
}
}
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的 T,而不是泛型類中的T。
萬用字元 ?
除了用 <T>
表示泛型外,還有 <?>
這種形式。? 被稱為萬用字元。
已經有了
class Base{}
class Sub extends Base{}
Sub sub = new Sub();
Base base = sub;
上面程式碼顯示,Base 是 Sub 的父類,它們之間是繼承關係,所以 Sub 的例項可以給一個 Base 引用賦值,那麼
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;
最後一行程式碼成立嗎?編譯會通過嗎?
答案是否定的。
編譯器不會讓它通過的。Sub 是 Base 的子類,不代表 List和 List有繼承關係。
萬用字元的出現是為了指定泛型中的類型範圍。
萬用字元有 3 種形式。
<?>
被稱作無限定的萬用字元。<? extends T>
被稱作有上限的萬用字元。<? super T>
被稱作有下限的萬用字元。