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包含的是原始数据,因此我们可以添加IntegerString。当我们使用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类型的数据添加到包含IntegerList中时,编译器会发出警告。

在泛型中,类型信息仅在编译时可用。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上找到。