Determine the Class of a Generic Type in Java – 确定 Java 中通用类型的类别

最后修改: 2023年 12月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Generics, which was released in Java 5, allowed developers to create classes, interfaces, and methods with typed parameters, enabling the writing of type-safe code. Extracting this type of information at runtime allows developers to write more flexible code.

Generics 在 Java 5 中发布,允许开发人员创建带有类型参数的类、接口和方法,从而能够编写类型安全的代码。在运行时提取此类信息可让开发人员编写更灵活的代码。

In this tutorial, we’ll learn how to get the class of a generic type.

在本教程中,我们将学习如何获取泛型的类。

2. Generics and Type Erasure

2.泛型和类型擦除

Generics were introduced in Java with the major goal of providing compile-time type-safety checks along with flexibility and reusability of code. The introduction of Generics made the Collections framework undergo significant enhancements and improvements. Before generics, Java collections used raw type, which was kind of error-prone, and developers often faced typecast exceptions.

Java 引入泛型的主要目的是提供编译时类型安全检查以及代码的灵活性和可重用性。引入泛型后,集合框架得到了显著的增强和改进。在引入泛型之前,Java 集合使用原始类型,这种类型容易出错,开发人员经常会遇到类型转换异常。

To demonstrate this, let’s consider a simple example where we create a List and add data to it:

为了演示这一点,让我们举一个简单的例子,创建一个 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 
    } 
}

In the above example, the List contains raw data. Hence, we’re able to add Integers and Strings. When we read the list using get(), we’re type-casting it to Integer, but for the String type, we get a type-casting exception.

在上述示例中,List 包含原始数据。因此,我们可以添加 Integers 和 Strings。当我们使用 get() 读取列表时, 我们将其类型转换为 Integer 类型,但对于 String 类型,我们会收到类型转换异常。

With generics, we define a type parameter for a collection. If we try to add any other data type than the defined type parameter, then the compiler complains about it.

通过泛型,我们为集合定义了一个类型参数。如果我们试图添加定义类型参数之外的任何其他数据类型,编译器就会抱怨。

For example, let’s create a generic List with an Integer type and try adding different types of data to it:

例如,让我们创建一个具有 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.
    }
}

In the above code, when we’re trying to add a String data type to the List of integers, the compiler complains about it.

在上面的代码中,当我们试图在整数 List 中添加 String 数据类型时,编译器会对此提出抱怨。

In Generics, the type of information is only available at compile-time. Java compiler erases type information during compilation and it’s not available at runtime. This is called type erasure. 

在 Generics 中,类型信息只在编译时可用。Java 编译器会在编译时擦除类型信息,运行时则不可用。这被称为类型擦除

Due to type erasure, all the type parameter information is replaced with the bound (if the upper bound is defined) or the object type (if the upper bound isn’t defined).

由于类型擦除,所有类型参数信息都会被边界(如果定义了上界)或对象类型(如果没有定义上界)取代

We can confirm this by using the javap utility, which inspects the .class file and helps to examine the bytecode. Let’s compile the code containing the withGenerics() method above and inspect it with the javap utility:

我们可以使用 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; 

As we can see in bytecode mnemonics, line #13,  List.add method was passed with Object instead of Integer type.

正如我们在 bytecode 助记符中看到的,第 13 行,List.add 方法传递的是 Object 而不是 Integer 类型。

Type erasure was a design choice made by Java designers to support backward compatibility.

类型擦除是 Java 设计人员为支持向后兼容性而做出的设计选择。

3. Getting Class Information

3.获取班级信息

The unavailability of type information at runtime makes it challenging to capture type information at runtime. However, there are certain workarounds to get the type information at runtime.

由于运行时无法获得类型信息,因此在运行时捕获类型信息具有挑战性。

3.1. Using Class<T> Parameter

3.1.使用 Class<T> 参数</b

In this approach, we explicitly pass the class of generic type T at runtime, and this information is retained so that we can access it at runtime. In the below example, we’re passing Class<T> at runtime to the constructor, which assigns it to the clazz variable. Then, we can access class information using the getClazz() method:

在这种方法中,我们在运行时显式地传递泛型 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;
    }
}

Our test verifies that we’re successfully storing and retrieving the class information at runtime:

我们的测试将验证我们是否能在运行时成功存储和检索类信息:

@Test
public void givenContainerClassWithGenericType_whenTypeParameterUsed_thenReturnsClassType(){
    var stringContainer = new ContainerTypeFromTypeParameter<>(String.class);
    Class<String> containerClass = stringContainer.getClazz();

    assertEquals(String.class, containerClass);
}

3.2. Using Reflection

3.2.使用反射</b

Using a non-generic field with reflection is another workaround that allows us to get generic information at runtime.

通过 reflection 使用非通用字段是另一种变通方法,它允许我们在运行时获取通用信息。

Basically, we use reflection to obtain the runtime class of a generic type. In the below example, we use content.getClass(), which gets class information of content at runtime using reflection:

基本上,我们使用反射来获取泛型的运行时类。在下面的示例中,我们使用了 content.getClass(),它可以在运行时通过反射获取内容的类信息:

public class ContainerTypeFromReflection<T> {
    private T content;

    public ContainerTypeFromReflection(T content) {
        this.content = content;
    }

    public Class<?> getClazz() {
        return this.content.getClass();
    }
}

Our test verifies that it works for the ContainerTypeFromReflection class and gets the type information:

我们的测试验证了它对 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. Using TypeToken

3.3.使用 TypeToken

Type tokens are a popular way to capture generic type information at runtime. It was made popular by Joshua Bloch in his book “Effective Java”.

类型标记是一种在运行时捕获通用类型信息的流行方法。Joshua Bloch 在他的著作《Effective Java》中介绍了这种方法。

In this approach, we first create an abstract class called TypeToken, where we pass the type information from the client code. Inside the abstract class, we then use the getGenericSuperClass() method to retrieve the passed type argument at runtime:

在这种方法中,我们首先创建一个名为 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;
    }
}

As we can see in the above example, Inside our TokenType abstract class, we are capturing Type info at runtime using getGenericSupperClass(), which we are returning using the getType() method.

正如我们在上例中看到的,在令牌类型抽象类中,我们使用 getGenericSupperClass() 方法在运行时捕获 类型信息,并使用 getType() 方法返回这些信息。

Our test verifies that it works for the sample class that extends the abstract TypeToken with String as the type parameter:

我们的测试验证了该方法适用于以 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. Conclusion

4.结论</b

In this article, we discuss generics and type erasure, along with its benefits and limitations. We also explored various workarounds for getting a class of generic type information at runtime, along with code examples.

在本文中,我们讨论了泛型和类型擦除及其优点和局限性。我们还探讨了在运行时获取类属类型信息的各种变通方法,并附有代码示例。

As always, the example code is available over on GitHub.

在 GitHub 上提供了示例代码。