泛型擦除:那运行时还能取到泛型的具体类型么?

199 阅读4分钟

实际上,类型擦除(type erasure)意味着 Java 编译器在编译时会去掉所有泛型类型的信息,但并不是说我们在运行时完全无法访问这些类型的相关信息。虽然在运行时无法直接获取List<T>T的具体类型,但可以使用一些技巧来获取泛型类型的相关信息。

泛型擦除的工作原理

在 Java 中,泛型类型擦除是为了兼容 Java 的原始类型(raw types)而引入的机制。具体而言,编译器会把所有泛型类型的类型参数在编译时替换为它们的边界类型(通常是Object,如果没有指定边界),然后生成不带泛型的字节码。

例如,List<String>List<Integer>在编译后都会变成List,因此在运行时,你无法直接获取到List<String>中的String类型,或者List<Integer>中的Integer类型。

泛型擦除的关键点

  1. 类型擦除后的类:编译器会将泛型参数(如T)擦除,并替换为它的上界,默认上界是Object,例如List<T>会变为List,并且所有类型参数都被擦除。

  2. 运行时泛型信息丢失:泛型的类型信息仅在编译时可见,在运行时并没有实际的泛型类型信息。例如,List<String>List<Integer>在运行时都是List,因此无法直接通过反射得到T的类型。

  3. 通配符和类型边界:如果泛型有边界(例如List<T extends Number>),在编译后类型擦除时,T会变成Number,并且在运行时可以推测T的类型。

  4. 使用反射:如果需要获取泛型类型,必须通过反射类型Token来进行操作。

泛型擦除相关知识点

  1. 类型擦除:所有泛型类型在编译时都会被擦除为它们的原始类型。比如List<String>List<Integer>都会被擦除为List,没有泛型信息。

  2. 反射和泛型:通过反射,尽管类型擦除丧失了泛型信息,但可以通过传递类对象或类型Token来在运行时获取泛型的类型。例如Class<T>Type可以用于获取泛型类型。

  3. 泛型的局限性

    • 不能在运行时直接创建带泛型的数组。
    • 不能直接实例化带泛型的类。
    • 泛型的类型信息在运行时不可见(类型擦除)。
  4. 泛型通配符

    • ? extends T表示上界通配符,类型为T或T的子类。
    • ? super T表示下界通配符,类型为T或T的父类。

代码示例

  1. 常见的泛型擦除现象

    import java.util.List;
    
    public class GenericTypeErasure {
        public static void main(String[] args) {
            List<String> list = List.of("Hello", "World");
    
            // 在编译时擦除,List<String>和List<Integer>都会被当作List
            System.out.println(list.getClass());  // 输出: class java.util.ImmutableList
        }
    }
    

    这里你可以看到,List<String>List<Integer>在运行时都是List类型,StringInteger的类型信息在运行时都被擦除掉了。

  2. 使用反射获取泛型类型

    如果你想在运行时获得泛型的类型信息,可以通过反射类型Token来实现。例如,使用Type接口和ParameterizedType来获取泛型的实际类型:

    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    import java.util.List;
    
    public class GenericTypeReflection {
        public static void main(String[] args) {
            List<String> list = List.of("Hello", "World");
    
            // 获取泛型类型信息
            Type type = ((ParameterizedType) list.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            System.out.println("泛型类型是: " + type);  // 输出: class java.lang.String
        }
    }
    

    上面的代码演示了如何通过反射来访问泛型类型(需要注意,这种方法适用于某些情况,尤其是当泛型类型是显式传递的时)。

  3. 通过类型Token传递泛型类型

    另一种方法是使用类型Token模式,将泛型类型信息保留在类的实例中:

    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    import java.util.List;
    
    public class TypeToken<T> {
        private final Type type;
    
        public TypeToken() {
            this.type = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        }
    
        public Type getType() {
            return type;
        }
    
        public static void main(String[] args) {
            TypeToken<List<String>> typeToken = new TypeToken<List<String>>() {};
            System.out.println("泛型类型是: " + typeToken.getType());  // 输出: java.util.List<java.lang.String>
        }
    }
    

    通过这种方式,Java能够保留泛型类型的信息,即使在运行时,泛型的类型参数仍然可用。

总结

  • 类型擦除确实会在编译后丢失泛型类型信息,但并不意味着无法获取泛型的实际类型。在某些情况下,你仍然可以通过反射或类型Token的方式,获取泛型类型的相关信息。
  • 反射类型Token是获取泛型类型信息的常用方法,尤其是在需要动态类型分析时。