泛型(Generic Type)是Java中重要的一部分。在J2SE 5.0之後新增。在使用Java標準庫中的內容的時候,經常會遇到泛型。這裏將泛型總結一下
什麼是泛型
- 討論一個內容的時候,首先會來說什麼是什麼。在官方的文檔中說到
A generic type is a generic class or interface that is parameterized over types.
- 泛型又可以稱作參數化類型,這是在Java SE新增添的特性。一對尖括號,中間包含類型信息。將類型獨立成參數,在使用的時候才指定實際的類型
如果沒有泛型會怎麼樣?我們考慮以下幾種情況:
你實現了一個存儲整數類型(Integer)的列表,這時候你又需要存儲字符串(String)的列表,兩種列表邏輯行爲完全一樣,只是存儲的類型不同
爲了保證列表的通用性,你將列表的類型改爲了Object,這樣就不用爲類型修改代碼了。但是每次從列表中取對象的時候都需要強制轉換,而且很很容易出錯
- 有了泛型之後,可以將邏輯相同類型不同的代碼獨立出來,由編譯器負責進行類型轉換
泛型的聲明
泛型方法(Generic Method)
- 泛型方法是在普通方法聲明上加入了泛型
public static < E > void printArray( E[] inputArray ){
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
- 調用:
// Create arrays of Integer, Double and Character
Integer[] intArray = { 1, 2, 3, 4, 5 };
Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };
System.out.println( "Array integerArray contains:" );
printArray( intArray ); // pass an Integer array
System.out.println( "\nArray doubleArray contains:" );
printArray( doubleArray ); // pass a Double array
System.out.println( "\nArray characterArray contains:" );
printArray( charArray ); // pass a Character array
- 輸出:
Array integerArray contains:
1 2 3 4 5 6
Array doubleArray contains:
1.1 2.2 3.3 4.4
Array characterArray contains:
H E L L O
Java泛型方法的聲明格式如下:
[權限] [修飾符] [泛型] [返回值] [方法名] ( [參數列表] ) {}
public static < E > void printArray( E[] inputArray ) {}
- 泛型的聲明,必須在方法的修飾符(public,static,final,abstract等)之後,返回值聲明之前。可以聲明多個泛型,用逗號隔開。泛型的聲明要用<>包裹
泛型方法的使用有兩種:
類型推導
- 以聲明鍵值對的例子來說,通常的寫法會有一長串,不免有些痛苦
Map<String, List<String>> m = new HashMap<String, List<String>>();
- 我們可以構造一個泛型方法作爲靜態工廠,來完成這一操作
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
Map<String, List<String>> m = newInstance();
- 編譯器在編譯代碼的時候推導出了K, V分別對應的類型。當然,編譯器的推導能力也是有限的,這裏也就不過多討論了
指定類型
- 不贅述
泛型類(Generic Class)
- 泛型類和普通類的聲明一樣,只是在類名後面加上了類型表示。就像泛型方法,泛型類可以有一個或多個類型表示,用逗號進行分隔
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();
integerBox.add(new Integer(10));
stringBox.add(new String("Hello World"));
System.out.printf("Integer Value :%d\n\n", integerBox.get());
System.out.printf("String Value :%s\n", stringBox.get());
}
}
Integer Value :10
String Value :Hello World
在泛型類上聲明的類型,可以被用到類中任何表示類型的地方
泛型類只能通過以指定類型的方式進行使用。在之後的Java版本中,加入了類型推導功能,可以將後面的泛型類型省略,但是還是需要保留尖括號
List<String> list = new ArrayList<String>(); // 普通的寫法
List<String> list = new ArrayList<>(); // 省略的寫法
泛型接口(Generic Interface)
- 泛型接口是在聲明接口的時候指定,類在繼承接口的時候需要補充泛型類型
public interface Info<T> {
public T getInfo();
}
- 然後定義一個類實現這個接口
public InfoImp implements Info<String> {
public String getInfo() {
return "Hello World!";
}
}
- 可以發現實現接口裏的方法需要使用具體的類型
泛型接口的一般格式:
[訪問權限] interface [接口名] <泛型標識> {}
- 當然,我們可以實現泛型接口的時候不指名泛型類型,這樣這個類就需要定義爲泛型類
public InfoImp<T> implements Info<T> {
public T getInfo() {
return null;
}
}
泛型標識與泛型通配符
- 理論上泛型標識可以使用任意的字母或字母序列,可以考慮以下的例子,但是不推薦這樣使用
public static <STRING> STRING sayHello(STRING word){
System.out.println("Hello " + word);
return a;
}
- 但是Java內部有一套自己的規範,這樣在閱讀JDK代碼的時候會更加明確泛型標識的含義
E - Element (在集合中使用,因爲集合中存放的是元素)
T - Type(Java 類)
K - Key(鍵)
V - Value(值)
N - Number(數值類型)
? - 表示不確定的java類型
S、U、V - 2nd、3rd、4th types
- 說到泛型標識符,再說一說泛型通配符。常用的泛型通配符有三種
任意類型 “<”?>
- “<”?> 可以理解爲泛型中的 Object,爲什麼這麼說呢?因爲任意類型的通配符可以接受任意類型的泛型。下面的例子表示出了這種關係
Box<?> box = new Box<String>();
- 類似於將後面的類型轉換到前面的類型。但是
class Box<?> {} //錯誤的泛型類定義
public static <?> void sayHello(? helloString) {} //錯誤的泛型方法定義
interface Box<?> {} //錯誤的泛型接口定義
上限類型 “<”? extends 類>
- “<”? extends 類> 表示泛型只能使用這個類或這個類的子類。舉個例子
public static <T extends String> void sayHello(T helloString) {}
在該方法中調用sayHello(“xiaoming”);是正確的,但是調用sayHello(2333);就是錯誤的
考慮一種更通用的情況
public static void printList(List<? extends String> list){
for(String str:list){
System.out.println(str);
}
}
- 這個例子指定了泛型類的具體類型的範圍。在JDK中經常可以看到這樣的使用方法
下限類型 “<”? super 類>
- 同上限方法,”<”? super 類> 表示泛型只能使用這個類或這個類的父類
類型擦除
泛型只在編譯時有效,編譯成字節碼的過程中會進行類型擦除的操作。當然並不是所有的泛型類型都被擦除,有些也會保留下來
一個簡單的類型擦除的例子:
List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
}
- 我們對其編譯,然後再反編譯,反編譯引擎用的CFR:
ArrayList arrayList = new ArrayList();
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
String string = (String)iterator.next();
}
從上面的結果可以看出,泛型類上面的類型被去掉了,但是增加了一個類型強制轉換。解釋器默認認爲裏面的類型都會是String。這是因爲在編譯的時候會進行類型檢查,如果發現使用的類型與泛型聲明類型不符,編譯是不會通過的
那能不能繞過這個檢查呢?這個時候就需要使用反射來進行操作了。就上面的例子來說,ArrayList可以放入任意類型,所以使用反射只要保證類型強制轉換不出問題,程序還是可以使用的
在Java文檔中提到,類型擦除主要進行以下工作:
將泛型中的所有類型參數更換成類型界限,或者無界的類型替換成Object。所以生成的字節碼只包含普通類、接口和方法
爲了確保類型安全,必要時插入強制類型轉換
生成橋接方法保持擴展泛型類型中的多態性
可變參數
- 使用泛型方法可以使用可變參數:
public class Main {
public static <T> void out(T... args) {
for (T t : args) {
System.out.println(t);
}
}
public static void main(String[] args) {
out("findingsea", 123, 11.11, true);
}
}
new T()?
- 能不能在泛型方法(類)中創建泛型類型的實例呢?答案是不可以的
public static <E> void append(List<E> list) {
E elem = new E(); // 編譯錯誤
list.add(elem);
}
- 不過可以使用反射來創建實例
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);
- 因爲類型擦除的緣故,部分類型信息會丟失,我們在運行時不會獲取到相應的類型,所以也就無法將該類型實例化成對象
泛型和數組
在Java中,直接創建泛型數組是非法的(要注意的一點是,Java中沒有所謂的泛型數組一說)。泛型設計的初衷是爲了簡化程序員類型轉換的操作,保證類型安全。數組是協變的,如果Sub爲Super的子類型,那麼數組Sub[]就是Super[]的子類型。這樣做就很難保證存儲上的安全
- 但在實際使用過程中,往往需要創建泛型數組:
public static <E> E[] newArray(int n) {
return new E[n];
}
- 這個時候運行程序,會拋出異常
Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot create a generic array of E
- 在這種情況下,可以使用 List 來代替數組
public static <E> List<E> newList() {
return new ArrayList<E>();
但是,Java不是生來就有List的,如果遇到必須使用數組的情況該怎麼辦?
在這裏可以參考Java裏聚合類型的實現,以一個簡單的例子說明
public class Stack<E> {
private E[] elements;
private int size = 0;
public Stack() {
elements = new E[10];
}
public void push(E e) {
elements[size++] = e;
}
public E pop() {
E result = elements[--size];
elements[size] = null;
return result;
}
}
上面的例子和java.util.Stack還是有區別的,只是爲了說明如何處理泛型數組問題
同樣運行的時候會出錯
Exception in thread “main” java.lang.Error: Unresolved compilation problem:
Cannot create a generic array of E
- 一種方法是在創建泛型數組的時候創建一個Object數組,然後轉換成E數組
elements = (E[]) new Object[10];
- 第二種方法將數組類型改爲Object,在彈出元素的時候進行轉換
public class Stack<E> {
private Object[] elements;
...
public E pop() {
E result = (E) elements[--size];
...
}
}
- 泛型數組的返回和傳遞
package generics;
public class GenericsArray {
/**
* 返回一個泛型數組
* @param ts
* @return
*/
@SafeVarargs
public static <T> T[] getArray(T...ts){
return ts;
}
/**
* 爲方法傳遞一個泛型數組
* @param param
*/
public static <T> void print(T[] param){
System.out.println("the Param is:");
for(T c:param){
System.out.print(c);
}
}
public static void main(String[] args){
Integer[] arr = getArray(1,2,3,4,5);
System.out.println(arr);
print(arr);
}
}