【问题标题】:Type erasure not working in Java Map class类型擦除在 Java Map 类中不起作用
【发布时间】:2017-09-27 10:27:20
【问题描述】:

我使用 javap 反编译了 Map 类。类定义仍然显示泛型类型 K 和 V 的存在。 这应该已经被类型擦除的概念擦除了。为什么这不会发生?

./javap -verbose java.util.Map

Classfile jar:file:/opt/jdk1.8.0_101/jre/lib/rt.jar!/java/util/Map.class
    Last modified 22 Jun, 2016; size 4127 bytes
    MD5 checksum 238f89b3e2ff9bebe07aa22b0a3493a9
    Compiled from "Map.java"

public interface java.util.Map<K extends java.lang.Object, V extends java.lang.Object>
    minor version: 0
    major version: 52
    flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT

Constant pool:

【问题讨论】:

  • 您正在查看调试信息。
  • 接口定义中还有泛型参数K和V,不应该已经被类型擦除的概念擦除了吗?
  • 是和不是。仍然包含一些关于类型参数和泛型超类型的元数据。必须有,否则你不能使用泛型类型,除非你有它们的源代码,因为编译器无法知道它们是泛型的。但元数据是类文件中的“额外”信息。 javap 使用此元数据向您展示通用签名而不是原始(已删除)签名。
  • 显然它正在工作,否则您将无法编译和执行您的代码。被擦除的类型在你的代码中,例如Map&lt;? extends Number&gt;,它被擦除为Mapjava/util/Map.class 中的内容无关紧要。
  • @user207421 为什么会不相关Map 本身是通用的,所以查看 class 文件对我来说非常有意义。并使用javap 查看该类(或任何其他泛型类)将显示保留类型的泛型信息

标签: java generics type-erasure


【解决方案1】:

如果泛型签名信息被完全删除,则不可能使用泛型类型或方法,除非您还拥有源代码。想一想:为了有效地使用泛型,编译器必须知道一个类型或方法是泛型的,并且它必须知道泛型参数的数量、位置和范围。

为此,javac 在本身是泛型的类型和方法上发出所谓的Signature 属性,或者其签名包含类型变量或其他泛型类型的实例化。

对于像Map&lt;K, V&gt; 这样的泛型类型,将发出类定义并带有Signature 属性描述:

  1. 类型声明的所有泛型参数(类型变量)及其边界;
  2. 类型基类的完整通用签名;
  3. 由该类型实现的接口的完整通用签名。

对于Map 接口,Signature 值如下所示:

<K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/lang/Object;

您可以在输出最末尾的javap -v 中看到此属性,在结束} 之后的行上。要查看更完整的泛型签名是什么样的,请查看HashMap 类,它具有泛型基类并实现了多个接口:

<K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/util/AbstractMap<TK;TV;>;Ljava/util/Map<TK;TV;>;Ljava/lang/Cloneable;Ljava/io/Serializable

从这个签名中,编译器知道关于类型HashMap的以下信息:

  1. 有两个泛型参数KV,它们都扩展了java.lang.Object
  2. 基类是java.util.AbstractMap&lt;K, V&gt;。澄清一下,这里的KV指的是HashMap定义的参数(不是AbstractMap)。
  3. 该类实现java.util.Map&lt;K, V&gt;java.lang.Cloneablejava.io.Serializable

方法也可能有Signature属性,但在方法的情况下,签名描述:

  1. 方法声明的所有泛型参数(类型变量)及其边界;
  2. 方法参数类型的完整通用签名;
  3. 方法返回类型的完整通用签名。

但是,方法的Signature 被认为是额外的元数据;您永远不会看到直接在字节码中引用。相反,您将看到对 方法描述符 的引用,它类似于已递归应用通用擦除的签名。与Signature 属性不同,方法描述符是必需的javap -v 很乐意向你们展示。例如,给定HashMap方法public V put(K, V)

  1. 方法描述符是(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
  2. 通用的Signature(TK;TV;)TV;

Signature 告诉编译器和您的 IDE 方法的完整通用签名,从而实现类型安全。 descriptor 是调用站点的字节码中实际引用该方法的方式。例如,给定表达式map.put(0, "zero"),其中mapMap&lt;Integer, String>,指令序列将类似于:

aload            (some variable holding a Map)
iconst_0
invokestatic     java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
ldc              "zero"
invokeinterface  java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

请注意如何没有保留通用信息。通过插入执行运行时强制转换的checkcast 指令在运行时强制执行有限类型安全。例如,在Map&lt;Integer, String&gt; 上调用map.get(0) 将包含类似于以下的指令序列:

aload            (some variable holding a Map)
iconst_0
invokestatic     java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
invokeinterface  java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
checkcast        Ljava/lang/String;

因此,即使 Map 类型在调用站点被完全擦除,发出的字节码确保从 Map&lt;Integer, String&gt; 检索到的任何值实际上String,而不是一些其他Object

需要强调的是,与类文件中的大多数元数据一样,Signature 属性完全可选。虽然javac 会在必要时发出它们,但它们有可能被字节码优化器和混淆器等后处理器剥离。当然,这将导致无法以预期的方式使用泛型。例如,如果您要去除java/util/Map.class 中的Signature 属性,则只能将Map 作为等效于Map&lt;Object, Object&gt; 的非泛型类使用,并且您必须自己处理类型检查。

【讨论】:

  • 太好了,你今天让开发者非常高兴。一直在寻找这样的答案 waaaay 太久了,非常感谢!
  • @Eugene 我发现你在这个网站上的许多答案和 cmets 很有帮助,所以我很高兴我能回报你的好意! :D
  • 如果您不介意,请提出后续问题。据我了解,如果编译器不会生成Signature,那么调用者就不可能知道某个方法是否是通用的,或者将checkcasts 插入其中,对吧?我想知道编译器以前会这样做吗?
  • @Eugene 当引用 compiled 类时,你是对的:没有Signature 属性,编译器不会知道类型或方法是通用的。显然,如果您有源文件,并且它们包含在编译中,编译器可以通过这种方式获取信息。我不确定您所说的“编译器如何之前”。如果您的意思是“在泛型之前”,则由编码人员来插入强制转换(例如,从集合中检索项目时)。
  • by before 我真的是说有没有编译器不会生成Signature,抱歉
【解决方案2】:

字节码中有额外的信息用于解码通用信息。

【讨论】:

    猜你喜欢
    • 2022-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-31
    • 1970-01-01
    • 2012-01-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多