键入擦除在Java Map类中不起作用

问题描述 投票:4回答:2

我使用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:
java generics type-erasure
2个回答
6
投票

如果完全删除了通用签名信息,除非您还拥有源代码,否则将无法使用泛型类型或方法。想一想:为了有效地使用泛型,编译器必须知道类型或方法是通用的,并且它必须知道泛型参数的数量,位置和边界。

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

对于像Map<K, V>这样的泛型类型,类定义将使用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<K, V>。为了澄清,KV在这里指的是HashMap(不是AbstractMap)定义的参数。
  3. 该类实现了java.util.Map<K, V>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该方法的完整通用签名,从而实现类型安全的强制执行。描述符是如何在调用站点的字节码中实际引用该方法。例如,给定表达式map.put(0, "zero"),其中mapMap<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.get(0)上调用Map<Integer, String>将包含类似于以下的指令序列:

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<Integer, String>检索的任何值实际上是String,而不是其他一些Object

重要的是要强调,与类文件中的大多数元数据一样,Signature属性是完全可选的。虽然javac会在必要时发出它们,但它们可能被后处理器(如字节码优化器和混淆器)剥离。当然,这将使得不可能以预期的方式使用泛型。例如,如果你要删除Signature中的java/util/Map.class属性,你只能使用Map作为一个等同于Map<Object, Object>的非泛型类,你必须自己处理类型检查。


0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.