確定 Java 中泛型類型的類
1. 概述
Java 5 中發布的泛型允許開發人員建立具有類型參數的類別、介面和方法,從而能夠編寫類型安全的程式碼。在運行時提取此類資訊允許開發人員編寫更靈活的程式碼。
在本教程中,我們將學習如何取得泛型類型的類別。
2. 泛型和類型擦除
Java 中引入泛型的主要目標是提供編譯時類型安全檢查以及程式碼的靈活性和可重複使用性。泛型的引入使得集合框架得到了顯著的增強和改進。在泛型之前,Java 集合使用原始類型,這很容易出錯,而且開發人員經常面臨類型轉換異常。
為了演示這一點,讓我們考慮一個簡單的範例,其中我們建立一個List
並向其中添加資料:
void withoutGenerics(){
List container = new ArrayList();
container.add(1);
container.add("2");
container.add("string");
for (int i = 0; i < container.size(); i++) {
int val = (int) container.get(i); //For "string", we get java.lang.ClassCastException: class String cannot be cast to class Integer
}
}
在上面的範例中, List
包含原始資料。因此,我們可以加入Integer
和String
。當我們使用get(),
我們將其類型轉換為Integer
,但對於String
類型,我們會遇到類型轉換異常。
使用泛型,我們為集合定義類型參數。如果我們嘗試添加定義的類型參數之外的任何其他資料類型,編譯器會抱怨它。
例如,讓我們建立一個具有Integer
類型的通用List
,並嘗試在其中新增不同類型的資料:
void withGenerics(){
List<Integer> container = new ArrayList();
container.add(1);
container.add("2"); // compiler won't allow this since we cannot add string to list of integer container.
container.add("string"); // compiler won't allow this since we cannot add string to list of integer container.
for (int i = 0; i < container.size(); i++) {
int val = container.get(i); // not casting required since we defined type for List container.
}
}
在上面的程式碼中,當我們嘗試將String
資料類型新增到整數List
時,編譯器會對此進行抱怨。
在泛型中,類型資訊僅在編譯時可用。 Java 編譯器在編譯期間會擦除類型訊息,並且在執行時不可用。這稱為類型擦除。
由於類型擦除,所有類型參數資訊都被替換為界限(如果定義了上限)或物件類型(如果未定義上限) 。
我們可以透過使用javap
實用程式來確認這一點,該實用程式檢查.class
檔案並幫助檢查字節碼。讓我們編譯包含上面withGenerics()
方法的程式碼並使用javap
實用程式檢查它:
javac CollectionWithAndWithoutGenerics.java // compiling java file
javap -v CollectionWithAndWithoutGenerics // read bytecode using javap tool
// bytecode mnemonics
public static void withGenerics();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: new #12 // class java/util/ArrayList
3: dup
4: invokespecial #14 // Method java/util/ArrayList."<init>":()V
7: astore_0
8: aload_0
9: iconst_1
10: invokestatic #15 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
13: invokeinterface #21, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;
正如我們在bytecode
助記符中看到的,第 13 行, List.add
方法是用Object
而不是Integer
類型傳遞的。
類型擦除是 Java 設計者為支援向後相容性而做出的設計選擇。
3. 獲取班級資訊
運行時類型資訊的不可用使得在運行時捕獲類型資訊變得具有挑戰性。但是,有一些解決方法可以在運行時獲取類型資訊。
3.1.使用Class<T>
參數
在這種方法中,我們在運行時明確傳遞泛型類型T
的類,並保留此訊息,以便我們可以在運行時存取它。在下面的範例中,我們在運行時將*Classclazz
變數。然後,我們可以使用getClazz()
方法來存取類別資訊:
public class ContainerTypeFromTypeParameter<T> {
private Class<T> clazz;
public ContainerTypeFromTypeParameter(Class<T> clazz) {
this.clazz = clazz;
}
public Class<T> getClazz() {
return this.clazz;
}
}
我們的測試驗證我們是否在運行時成功儲存和檢索類別資訊:
@Test
public void givenContainerClassWithGenericType_whenTypeParameterUsed_thenReturnsClassType(){
var stringContainer = new ContainerTypeFromTypeParameter<>(String.class);
Class<String> containerClass = stringContainer.getClazz();
assertEquals(String.class, containerClass);
}
3.2.使用反射
使用帶有反射的非通用字段是另一種解決方法,它允許我們在運行時獲取通用資訊。
基本上,我們使用反射來取得泛型類型的運行時類別。在下面的範例中,我們使用content.getClass(),
它在運行時透過反射來獲取內容的類別資訊:
public class ContainerTypeFromReflection<T> {
private T content;
public ContainerTypeFromReflection(T content) {
this.content = content;
}
public Class<?> getClazz() {
return this.content.getClass();
}
}
我們的測試驗證它是否適用於ContainerTypeFromReflection
類別並取得類型資訊:
@Test
public void givenContainerClassWithGenericType_whenReflectionUsed_thenReturnsClassType() {
var stringContainer = new ContainerTypeFromReflection<>("Hello Java");
Class<?> stringClazz = stringContainer.getClazz();
assertEquals(String.class, stringClazz);
var integerContainer = new ContainerTypeFromReflection<>(1);
Class<?> integerClazz = integerContainer.getClazz();
assertEquals(Integer.class, integerClazz);
}
3.3.使用TypeToken
類型標記是在運行時捕獲通用類型資訊的流行方法。 Joshua Bloch 在他的《Effective Java》一書中讓它變得流行。
在這個方法中,我們首先建立一個名為TypeToken,
在其中傳遞來自客戶端程式碼的類型資訊。在抽象類別中,我們使用getGenericSuperClass()
方法在執行時期檢索傳遞的類型參數:
public abstract class TypeToken<T> {
private Type type;
protected TypeToken(){
Type superClass = getClass().getGenericSuperclass();
this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
正如我們在上面的範例中看到的,在 TokenType 抽象類別中,我們在運行時使用getGenericSupperClass(),
捕獲Type
信息,並使用getType()
方法返回該信息。
我們的測試驗證了它適用於以String
作為類型參數擴展抽象TypeToken
範例類別:
@Test
public void giveContainerClassWithGenericType_whenTypeTokenUsed_thenReturnsClassType(){
class ContainerTypeFromTypeToken extends TypeToken<List<String>> {}
var container = new ContainerTypeFromTypeToken();
ParameterizedType type = (ParameterizedType) container.getType();
Type actualTypeArgument = type.getActualTypeArguments()[0];
assertEquals(String.class, actualTypeArgument);
}
4。結論
在本文中,我們討論泛型和類型擦除,及其優點和限制。我們還探索了在運行時獲取一類泛型類型資訊的各種解決方法以及程式碼範例。
與往常一樣,範例程式碼可以在 GitHub 上取得。