代码之家  ›  专栏  ›  技术社区  ›  wlnirvana ptomli

为什么在Java中成员字段的泛型类型信息没有被擦除?

  •  1
  • wlnirvana ptomli  · 技术社区  · 3 年前
    import java.lang.reflect.Field;
    import java.lang.reflect.Type;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Foo<T> {
    
        public List<Integer> aGenericList;
        
        public T item;
        
        public Foo() {
            aGenericList = new ArrayList<>();
        }
    
        public static void main(String[] args) throws NoSuchFieldException {
            Foo foo = new Foo<String>();
            System.out.println(foo.aGenericList.getClass());
    
            Field testField = Foo.class.getField("aGenericList");
            Type genericType1 = testField.getGenericType();
            System.out.println(genericType1.getTypeName());
        }
    }
    

    结果是:

    class java.util.ArrayList
    java.util.List<java.lang.Integer>
    

    这意味着通过反射方法可以获得擦除类型信息。

    现在我的问题是:

    1. 这种行为是在JLS/JVMS规范中正式定义的(如果是,在哪里?),还是取决于实现该语言的不同供应商?
    2. 是否可以将反射方法应用于局部变量 foo 得到类似的东西 Foo<java.lang.String> ?
    1 回复  |  直到 3 年前
        1
  •  4
  •   Slaw    3 年前

    您的问题

    1.此行为是否在JLS/JVMS规范中正式定义(如果是,在哪里?),还是取决于实现该语言的不同供应商?

    这个 Java语言规范 似乎特别 not describe reflection :

    因此,本说明书没有详细描述反射。

    但是,将反射的全部行为留给API记录(即在Javadoc中)。

    然而 Java虚拟机器规格 确实解释了这一点 generic information must be emitted by a compiler :

    4.7.9. 这个 Signature 属性

    这个 签名 attribute是a的属性表中的固定长度属性 ClassFile , field_info ,或 method_info 结构(§4.1、§4.5、§4.6)。A. 签名 属性记录了类、接口、构造函数、方法或字段的签名(§4.7.9.1),其在Java编程语言中的声明使用类型变量或参数化类型。请参阅 Java语言规范,Java SE 15版 有关这些构造的详细信息。

    [...]

    4.7.9.1. 签名

    签名 对使用Java虚拟机类型系统之外的类型的Java编程语言编写的声明进行编码。它们支持反射和调试,以及仅在特定情况下进行编译 class 文件可用。

    Java编译器必须为其声明使用类型变量或参数化类型的任何类、接口、构造函数、方法或字段发出签名。具体来说,Java编译器必须发出:

    • 任何类或接口声明的类签名,可以是泛型的,也可以具有参数化类型作为超类或超接口,或者两者兼而有之。

    • 任何方法或构造函数声明的方法签名,该声明要么是泛型的,要么具有类型变量或参数化类型作为返回类型或形式参数类型,要么在 throws 条款或其任何组合。

      如果 投掷 方法或构造函数声明的子句不涉及类型变量,那么编译器可能会将该声明视为没有类型变量 投掷 子句,用于发出方法签名。

    • 任何类型使用类型变量或参数化类型的字段、形式参数或局部变量声明的字段签名。

    [...]

    2.是否可以将反射方法应用于局部变量 foo 得到类似的东西 Foo<java.lang.String> ?

    否,因为局部变量不可反射访问。至少不是直接用Java语言。但假设他们是。您有:

    Foo foo = new Foo<String>();
    

    将反映的是左手侧。这是一种原始类型,所以你只知道 foo Foo 。您无法判断右侧创建的实例是用参数化的 String .


    一些澄清(希望)

    当我们说“泛型在运行时被擦除”时,我们并不是指在这种情况下。静态定义的可反射访问的构造类型(如字段)保存在字节码中。例如,以下内容:

    import java.lang.reflect.Field;
    import java.lang.reflect.ParameterizedType;
    import java.lang.reflect.Type;
    import java.lang.reflect.WildcardType;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    
      private static List<? extends Number> list = new ArrayList<Integer>();
    
      public static void main(String[] args) throws Exception {
        Field field = Main.class.getDeclaredField("list");
    
        // Due to List being a generic type the returned Type is actually
        // an instance of java.lang.reflect.ParameterizedType
        Type genericType = field.getGenericType();
        System.out.println("Generic Type  = " + genericType);
    
        // The raw type can be gotten from the ParameterizedType. Here the
        // returned Type will actually be an instance of java.lang.Class
        Type rawType = ((ParameterizedType) genericType).getRawType();
        System.out.println("Raw Type      = " + rawType);
    
        // The ParameterizedType gives us access to the actual type
        // arguments declared. Also, since a bounded wildcard was used
        // the returned Type is actually an instance of
        // java.lang.reflect.WildcardType
        Type typeArgument = ((ParameterizedType) genericType).getActualTypeArguments()[0];
        System.out.println("Type Argument = " + typeArgument);
    
        // We know in this case that there is a single upper bound. Here
        // the returned Type will actually be an instance of java.lang.Class
        Type upperBound = ((WildcardType) typeArgument).getUpperBounds()[0];
        System.out.println("Upper Bound   = " + upperBound);
      }
    }
    

    将输出:

    Generic Type  = java.util.List<? extends java.lang.Number>
    Raw Type      = interface java.util.List
    Type Argument = ? extends java.lang.Number
    Upper Bound   = class java.lang.Number
    

    所有这些信息都在源代码中。请注意,我们正在反思 list 领域 我们是 查看所述字段引用的实例(即运行时对象)。知道字段的泛型类型与知道字段的名称没有什么不同 列表 .

    我们不知道的是 ArrayList 参数化为 Integer .将上述内容更改为:

    import java.lang.reflect.TypeVariable;
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
    
      private static List<? extends Number> list = new ArrayList<Integer>();
    
      public static void main(String[] args) {
        Class<?> clazz = list.getClass();
        System.out.println("Class          = " + clazz);
    
        TypeVariable<?> typeParameter = clazz.getTypeParameters()[0];
        System.out.println("Type Parameter = " + typeParameter);
      }
    }
    

    输出:

    Class          = class java.util.ArrayList
    Type Parameter = E
    

    我们可以看到,我们知道引用的实例 列表 是以下实例 java.util.ArrayList 但从那里我们只能确定 数组列表 类是泛型的,只有一个类型参数 E 我们无法确定 列表 字段被分配了 数组列表 类型参数为 整数 换言之 数组列表 实例本身不知道它声明包含该信息的元素类型已被擦除。

    换句话说 列表 字段的类型在运行时是已知的,但 数组列表 实例(即对象) 在运行时创建 )只知道这是一个 数组列表 .

        2
  •  1
  •   Bohemian    3 年前

    类型 List<Integer> 是编译时常数。当这种情况发生时,编译器会烘焙该类型。

        3
  •  1
  •   Eugene    3 年前

    不,您仍然无法找到已擦除的类型信息(除非 via a trick 如果你想得到它 Foo<java.lang.String> ). 我不知道为什么,但这似乎更容易回答。您所要做的就是阅读以下文档 getGenericType :

    一个表示 声明类型 对于此field对象表示的字段。

    不是 实际类型 ,但是 声明类型 .

    如果你对代码进行反编译( javap -v -p -c ),您将在下面看到两个重要字段 aGenericList :

    public java.util.List<java.lang.Integer> aGenericList;
    descriptor: Ljava/util/List;
    flags: (0x0001) ACC_PUBLIC
    Signature: #17                          // Ljava/util/List<Ljava/lang/Integer;>;
    

    Signature descriptor 第二个是它的用途 呼叫站点 ,第一个确保编译器实际正确使用泛型。举个例子:

    static Map<Integer, String> map = new HashMap<>();
    
    public static void add(Integer x, String y) {
       map.put(x, y);
    }
    
    public String get(Integer x) {
       return map.get(x);
    }
    

    如果你反编译 get ,你会看到(除其他事项外):

    4: invokeinterface #19,  2 // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
    9: checkcast     #23      // class java/lang/String
    

    编译器如何知道插入 checkcast ? 因为完整的通用信息保留在 签名 现场。