【问题标题】:Why is this generic type information for member field not erased in Java?为什么在 Java 中没有删除成员字段的这种通用类型信息?
【发布时间】:2021-01-10 08:07:35
【问题描述】:
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&lt;java.lang.String&gt; 的内容?

【问题讨论】:

  • 您的意思是将aGenericList 声明为List&lt;T&gt; 而不是List&lt;Integer&gt;
  • @user2357112supportsMonica 我的意思是List&lt;Integer&gt;,但你的建议确实很有趣。稍后我可能会发布关于该案例的另一个问题。
  • 这里似乎很难区分编译时间和运行时间。比如局部变量Foo foo = new Foo&lt;String&gt;();呢?如果我们将List&lt;? extends Number&gt; aLocalGenericList = new ArrayList&lt;Integer&gt;(); 作为局部变量呢?是否可以在运行时获取StringNumberInteger 信息?

标签: java generics type-erasure


【解决方案1】:

您的问题

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

Java 语言规范似乎专门针对not describe reflection

因此,本规范没有详细描述反射。

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

但是,Java 虚拟机规范确实解释了generic information must be emitted by a compiler

4.7.9。 Signature 属性

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

[...]

4.7.9.1。签名

签名对用 Java 编程语言编写的声明进行编码,这些声明使用 Java 虚拟机类型系统之外的类型。它们支持反射和调试,以及只有class 文件可用时的编译。

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

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

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

    如果方法或构造函数声明的 throws 子句不涉及类型变量,则编译器可能会将声明视为没有 throws 子句以发出方法签名。

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

[...]

2。是否可以将反射方法应用于局部变量foo 以获得类似Foo&lt;java.lang.String&gt; 的内容?

不,因为局部变量不能通过反射访问。至少不是直接由 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 字段。我们不是查看所述字段引用的实例(即运行时对象)。知道该字段的泛型类型实际上与知道该字段的名称是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

我们可以看到我们知道list引用的实例是java.util.ArrayList的实例。但是从那里我们可以确定的是ArrayList 类是通用的并且具有单个类型参数E。我们无法确定list 字段是否被分配了ArrayList 类型参数为Integer。换句话说,ArrayList 实例本身不知道它声明包含什么类型的元素——该信息已被删除。

换句话说,list 字段的类型在运行时是已知的,但 ArrayList 实例(即对象在运行时创建)只知道它是 @987654358 @。

【讨论】:

  • 关键要点:擦除仅适用于运行时实例,不适用于静态声明的字段。非常感谢您的详细回答。
  • @wlnirvana 你总能找到那种类型,but you need a trick
  • @Eugene 好吧,是的,但是这个“技巧”本质上是使类非泛型。不过还是很酷。
【解决方案2】:

List&lt;Integer&gt; 的类型是编译时间常数。发生这种情况时,编译器会写入类型。

【讨论】:

    【解决方案3】:

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

    一个 Type 对象,表示此 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;>;
    

    Signaturedescriptor。第二个是在 调用站点 使用的,第一个确保编译器实际正确使用泛型。比如说这样一个例子:

    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?因为完整的通用信息保留在Signature 字段中。

    【讨论】:

    • 很酷的把戏!感谢您的进一步澄清。
    猜你喜欢
    • 1970-01-01
    • 2021-01-18
    • 1970-01-01
    • 2017-05-13
    • 1970-01-01
    • 2015-09-11
    • 1970-01-01
    • 2021-01-09
    • 2014-04-24
    相关资源
    最近更新 更多