Java 泛型 Generic機制實例詳解
泛型是什麼?
使用泛型可以指定類型變量,從而讓代碼可以對不同類型的對象進行重用。以及,還可以讓編譯器更好的瞭解類型,從而避免強制類型轉換,提升代碼的安全性。
類型變量就是尖括號 <>
中的變量,類型變量的命名規范是使用大寫字母,例如 E 表示元素類型,K、V 分別表示鍵和值類型,T 和相鄰的 U、S 表示任意類型。當然你也可以起其他的名字,編譯器對此並沒有強制限制,但是還是按照規范來。
泛型類
泛型類(generic class)就是有一個或多個類型變量的類。例如下面的 Pair 類,就有兩個類型變量,分別是 T 和 U:
public class Pair<T, U> { public T first; public U second; public Pair(T first, U second) { this.first = first; this.second = second; } }
在使用時,就可以傳入任意的類型。例如:
Pair<Integer, String> pair = new Pair<>(1, "A");
思考一下,如果沒有泛型的存在,那麼當我們要操作不同類型的對象時,要麼為每種類型都創建一個類,但是這樣就會存在大量的重復代碼,不容易維護。要麼就直接使用對象的頂級父類 Object 類,但是這樣的話在使用時就需要進行強制轉換,存在安全問題,即使每次強轉前進行判斷,也會存在重復代碼和遺漏的風險。
說回泛型類,泛型類的類型變量是定義在類名後面,在整個類中都可以使用定義的類型變量。所以在選擇使用泛型類時,泛型變量應該是和類所關聯的。如果僅與方法關聯,那麼可以使用泛型方法。
泛型方法
泛型方法的類型參數是定義在方法返回類型的前面的,隻在當前方法中可以使用。例如:
public static <T> T getMiddle(T... a) { return a[a.length / 2]; }
泛型方法可以在普通類中定義,也可以在泛型類中定義。如果泛型變量僅在方法內會用到,就可以考慮使用泛型方法。
類型變量的限制
默認情況下,類型變量可以是任何類型。但是有時候,我們需要限制類型變量的類型。此時可以通過 extends
關鍵字來限制:
<T extends UpperBoundType>
限制泛型類型為特定類型或者特定類型的子類
例如,假設我們有一個 Printer 類,我們隻需要這個類打印動物的信息,那麼就可以使用 <T extends Animal>
來限制隻能接收 Animal 或其實現類:
public interface Animal { String getName(); } public static class Printer<T extends Animal> { public void print(T t) { System.out.println(t.getName()); } } public static class Dog implements Animal { @Override public String getName() { return "Woof"; } } public static class Cat implements Animal { @Override public String getName() { return "Meow"; } } public static void main(String[] args) { Printer<Animal> animalPrinter = new Printer<>(); animalPrinter.print(new Dog()); animalPrinter.print(new Cat()); animalPrinter.print(new People("Abc")); // 如果試圖傳入一個其他類型,則會編譯失敗 }
一個類型變量,可以指定多個限制類型。例如:
public class Printer<T extends Dog & Animal> { ... }
上面的限制的意思是,類型變量必須是 Dog 或其子類,並且實現瞭 Animal 接口。需要註意的是,在指定多個限制的類型時,除瞭第一個限制類型可以是類以外,其他的限制類型必須是接口類型。這是因為 Java 是不支持多重繼承的。
類型擦除
由於泛型是在 Java 1.5 才推出的,所以 Java 為瞭保證在之前版本上的兼容性,泛型實際上在 JVM 中是沒有的!
也就是說,編譯器在編譯時會擦除掉泛型的參數類型,並提供一個相應的原始類型(raw type),這個原始類型的名字就是去掉類型參數後的泛型類型名。類型變量會被擦除(erased),並替換為其限定類型,沒有限定類型則替換為 Object 類型。
舉個例子,如果我們定義瞭一個泛型類:
public class Printer<T> { ... }
那麼當類型擦除後,實際上就變為瞭 Printer<Object>
類型。
如果指定瞭限定類型,例如:
public class Printer<T extends Animal> { ... }
那麼擦除後,就變為瞭 Printer<Animal>
類型。
如果指定瞭多個限定的類型,那麼擦除後就會是第一個限定的類型。例如:
public class Printer<T extends Dog & Animal> { ... }
擦除後就變為瞭 Printer<Dog>
類型。
Java 泛型的很多限制都是由於泛型擦除導致的,下面會詳細介紹下泛型的局限性。
通配符
通配符(Wildcard)就是 Java 泛型中的問號 ? 。要想知道通配符是要解決什麼樣的問題,我覺得首先需要知道什麼是 Variance(不知道這個正確的譯名是什麼,如有知道請留言)?
提到 Variance 必須先提到子類型(subtyping),子類型是面向對象中類型多態的其中一種表現形式,主要用於描述 is-a 這樣的關系,例如 S 是 T 的子類型,那麼 S is a subtype of T。
Variance 指的是如何根據組成類型之間的子類型關系,來確定更復雜的類型之間的子類型關系。
上面這段話比較繞,舉個實際的例子來說,Variance 指的就是當子類型在更復雜的場景下,例如 If S is a subtype of T,那 Generic<S> is subtype of Generic<T>
這種關系是否還能成立。
Variance 分為下面幾種形式:
- 不變(Invariance):如果 B 是 A 的子類型,那麼
Generic<B>
不是Generic<A>
的子類型。 - 協變(Covariance):如果 B 是 A 的子類型,那麼
Generic<B>
是Generic<A>
的子類型。 - 逆變(Contravariance):如果 B 是 A 的子類型,那麼
Generic<A>
是Generic<B>
的子類型(逆變就是逆轉瞭子類型關系)。 - 雙變(Bi-variance):Java 中不存在雙變,因此不討論。
在 Java 當中,泛型是不變的,這也就意味著下面這段看似沒問題的代碼會報錯:
List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; // 編譯報錯
因為按照不變的說明,List<Dog>
根本就不是 List<Animal>
的子類型,所以自然就不能賦值瞭。
Java 中的泛型采用不變性是為瞭保證類型安全。不過不變性雖然保證瞭類型安全,但是也讓靈活性大大降低。為此,Java 提供瞭通配符,可以解除部分這種限制,也不損失安全性。
上界通配符
上界通配符的形式為:? extends Type
,使用上限通配符可以將類型參數變為協變的,這就可以讓原本無法賦值的類型可以正常賦值瞭。但是這在解除不變性限制的同時,也帶來瞭新的限制,就是參數類型隻能作為返回值類型,無法作為入參的類型。例如:
List<? extends Animal> animals = new ArrayList<Dog>(); // 可以正確賦值瞭 animals.add(new Dog()); // 編譯錯誤,類型不匹配,無法寫入,除非寫入 null(也就是參數類型無法作為入參的類型) Animal animal = animals.get(0); // 可以獲取(也就是參數類型可作為返回值類型)
這就叫當上帝給你關瞭一扇門,同時會幫你打開一扇窗。盡管窗戶有個洞,但總比沒有好吧?你說是不是?
下界通配符
下界通配符的形式為:? super Type
,使用下界通配符可以將類型參數變為逆變的,也就是說顛倒瞭類型關系,父類型可以賦值給子類型瞭!例如:
List<Animal> animals = new ArrayList<>(); List<? super Dog> dogs = animals; // 可以正確賦值!
看起來挺神奇的吧!這種方式同樣也帶來瞭新的限制,在使用這種變量的時候,不可以調用返回值包含類型參數的方法,也不能獲得包含類型參數的字段值。簡單來說,就是隻能作為輸入,但是不能輸出。例如:
dogs.add(new Dog()); // 可以寫入 Dog dog = dogs.get(0); // 編譯報錯,類型不匹配: Required type: Dog Provided: capture of ? super Dog
對於獲取時編譯報類型不匹配的問題,其實可以通過類型強制轉換來解決。
無界通配符
無界通配符的形式就是一個單獨的問號 ?。當我們對類型參數一無所知,但仍然希望以安全的方式使用它時,則可以考慮使用無界通配符。例如:
public static boolean hasNull(Pair<?> p) { return p.first != null && p.second != null; }
反射和泛型
因為泛型擦除,所以無法在反射時獲取泛型類型。但是擦除的類依然保留原先泛型的微弱記憶。例如,如果定義瞭以下泛型方法:
public static <T extends Comparable<? super T>> T min(T[] a)
那麼,可以通過反射可以獲取到以下信息:
public static Comparable min(Comparable[] a)
也就是說,我們可以重新構造實現者聲明的泛型類和方法的有關內容。但是,不會知道對於特定的對象或方法調用會如何解析類型參數。
為瞭表述泛型類型聲明,可以使用 java.lang.reflect
包中的接口 Type
。這個接口包含以下子類型:
- Class 類:描述具體類型
- TypeVariable 接口:描述類型變量(如
T extends Comparable<? super T>
) - WildcardType 接口:描述通配符(如
? super T
) - ParameterizedType 接口:描述泛型類或接口類型(如
Comparable<? super T>
) - GenericArrrayType 接口:描述泛型數組(如
T[]
)
下面的這個示例程序,就是通過反射來打印指定類的信息:
public class GenericReflectionTest { public static void main(String[] args) { String className; try (Scanner scanner = new Scanner(System.in)) { System.out.println("Enter class name (e.g., java.util.Collections): "); className = scanner.next(); } try { Class<?> cl = Class.forName(className); ClassPrinter.print(cl); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static class ClassPrinter { public static void print(Class<?> cl) { System.out.print(cl); printTypes(cl.getTypeParameters(), "<", ", ", ">", true); final Type sc = cl.getGenericSuperclass(); if (sc != null) { System.out.print(" extends "); printType(sc, false); } printTypes(cl.getGenericInterfaces(), " implements ", ", ", "", false); System.out.print(" {"); System.out.println(); final Method[] methods = cl.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { final Method method = methods[i]; printMethod(method); if (i < methods.length - 1) { System.out.println(); } } System.out.println(); System.out.print("}"); } private static void printTypes(Type[] types, String pre, String sep, String suf, boolean isDefinition) { if (pre.equals(" extends ") && Arrays.equals(types, new Type[]{Object.class})) { return; } if (types.length > 0) { System.out.print(pre); } for (int i = 0; i < types.length; i++) { if (i > 0) { System.out.print(sep); } printType(types[i], isDefinition); } if (types.length > 0) { System.out.print(suf); } } private static void printType(Type type, boolean isDefinition) { if (type instanceof Class) { // 具體類型 Class<?> cl = (Class<?>) type; System.out.print(cl.getName()); } else if (type instanceof TypeVariable) { // 類型變量 TypeVariable<?> tv = (TypeVariable<?>) type; System.out.print(tv.getName()); if (isDefinition) { printTypes(tv.getBounds(), " extends ", " & ", "", false); } } else if (type instanceof WildcardType) { // 通配符 WildcardType wt = (WildcardType) type; System.out.print("?"); printTypes(wt.getUpperBounds(), " extends ", " & ", "", false); printTypes(wt.getLowerBounds(), " super ", " & ", "", false); } else if (type instanceof ParameterizedType) { // 泛型類或接口類型 ParameterizedType pt = (ParameterizedType) type; Type ownerType = pt.getOwnerType(); if (ownerType != null) { printType(ownerType, false); } printType(pt.getRawType(), false); printTypes(pt.getActualTypeArguments(), "<", ", ", ">", false); } else if (type instanceof GenericArrayType) { // 泛型數組 GenericArrayType gat = (GenericArrayType) type; printType(gat.getGenericComponentType(), isDefinition); System.out.print("[]"); } else { System.out.print("unknown type: " + type); } } private static void printMethod(Method m) { final String name = m.getName(); System.out.print(" "); System.out.print(Modifier.toString(m.getModifiers())); System.out.print(" "); printTypes(m.getTypeParameters(), "<", ", ", ">", true); printType(m.getGenericReturnType(), false); System.out.print(" "); System.out.print(name); System.out.print("("); printTypes(m.getGenericParameterTypes(), "", ", ", "", false); System.out.print(")"); } } }
以下面的這個測試類為例:
public class TestClass<T extends Comparable<? super T>> implements Cloneable { public void test(T t) { System.out.println(t); } }
運行上面的程序的輸出結果是:
class com.airsaid.java.samples.generic.TestClass<T extends java.lang.Comparable<? super T>> extends java.lang.Object implements java.lang.Cloneable { public void test(T) }
可以看到,可以獲取到該泛型類所定義的泛型信息。
類型字面量
有時候,我們確實需要在運行時獲取泛型的參數類型,那麼真的就沒有辦法瞭嗎?
答案是有的,在上面反射的例子中我們可以看到,字節碼當中其實保存瞭部分的泛型信息(簽名信息)。
舉一個實際場景的例子,Gson 這個框架相信大傢都用過。在進行反序列化時,有多個重載方法:
fromJson(String json, Class cl)
fromJson(String json, TypeToken type)
當使用第一個方法時,如果第二個參數是一個泛型類的 class 對象,那麼就會出現問題。例如下面的這段示例代碼:
Foo<DataBean> foo = new Foo<DataBean>(); gson.fromJson(json, foo.getClass());
這是因為無法獲取到 Foo 類的泛型參數 DataBean。
因此,Gson 提供瞭 TypeToken 類來處理這種問題。使用起來是這樣的:
Foo<DataBean> foo = new Foo<DataBean>(); gson.fromJson(json, new TypeToken<Foo<DataBean>>(){});
下面是我簡化過後的 TypeToken 源碼:
public class TypeToken<T> { private final Type type; protected TypeToken() { this.type = getTypeTokenTypeArgument(); } public Type getType() { return type; } private Type getTypeTokenTypeArgument() { final Type superclass = getClass().getGenericSuperclass(); if (superclass instanceof ParameterizedType) { ParameterizedType parameterized = (ParameterizedType) superclass; if (parameterized.getRawType() == TypeToken.class) { return parameterized.getActualTypeArguments()[0]; } } else if (superclass == TypeToken.class) { throw new IllegalStateException("TypeToken must be created with a type argument: new TypeToken<...>() {}; " + "When using code shrinkers (ProGuard, R8, ...) make sure that generic signatures are preserved."); } throw new IllegalStateException("Must only create direct subclasses of TypeToken"); } }
TypeToken 的原理就是創建瞭一個匿名子類對象,然後通過 getGenericSuperclass()
方法獲取其泛型父類,再獲取其中定義的參數化類型的類型參數。通過這種方式,就可以獲取到 TypeToken 所定義的泛型類型瞭:
public class TypeTokenTest { public static void main(String[] args) { TypeToken<?> typeToken = new TypeToken<List<String>>() {}; System.out.println(typeToken.getType()); // java.util.List<java.lang.String> } }
限制和局限性
不能使用基本類型實例化類型參數
泛型的類型參數不可以使用基本類型,例如 Pair<int>
,這是因為類型擦除後類型變為瞭 Object,而 Object 無法存儲基本類型數值。對於這種情況,我們需要使用基本類型對應的包裝類型。
運行時類型查詢隻適用於原始類型
同樣也是由於類型擦除導致的問題,如果要在代碼中進行判斷是否是某個泛型類型,這是無法做到的。例如:
if (a instanceof Pair<String>) // 錯誤
不能創建參數化類型的數組
還是由於類型擦除導致的問題,無法實例化參數化類型的數組。例如:
Pair<String>[] table = new Pair<String>[10]; // 錯誤
不能創建 Throwable 的子類為泛型類
如果希望創建一個 Throwable 的子類,那麼子類就不可以是泛型類。例如:
class Generic<T> extends Exception { }
此時,編譯器會直接報錯,提示:Subclass of 'Throwable' may not have type parameters
。
這還是因為泛型擦除,這就導致在 catch 時不知道該執行哪個代碼分支:
try { ... } catch (Generic<String> e) { } catch (Generic<Integer> e) { }
以上就是Java 泛型 Generic機制實例詳解的詳細內容,更多關於Java 泛型 Generic的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Java 中泛型 T 和 ? 的區別詳解
- 一篇文章帶你深入瞭解Java對象與Java類
- Java中泛型學習之細節篇
- Java基礎之java泛型通配符詳解
- Java 泛型詳解(超詳細的java泛型方法解析)