1. 概述
Java 5引入的泛型(Generics)使得开发者能够创建带有类型参数的类、接口和方法,从而编写出类型安全的代码。在运行时提取这些类型信息,有助于编写更具灵活性的代码。
在这个教程中,我们将学习如何获取泛型类型的类。
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()
读取列表时,对于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
类型的数据添加到包含Integer
的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;
如字节码助记符所示,第13行的List.add
方法接收到的是Object
,而不是Integer
类型。
类型擦除是Java设计者为了支持向后兼容性做出的设计选择。
3. 获取类型信息
由于运行时无法获取类型信息,这给在运行时捕获类型信息带来了挑战。然而,有一些方法可以解决这个问题。
3.1. 使用Class<T>
参数
在这种方法中,我们在运行时明确传递泛型类型T
的类,并保留这些信息以便于在运行时访问。在下面的示例中,我们通过构造函数在运行时传递Class<T>
,并将它赋值给clazz
变量。然后,我们可以使用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
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()
在运行时捕获类型信息,然后通过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上找到。