您说得对,在字节码级别上,定义和使用泛型时会丢失很多信息。类型擦除的好处在于保持兼容性:如果您大部分时间都在编译时强制实施类型安全性,那么在运行时就不需要做太多操作,因此可以将泛型类型简化为其“原始”等效类型。
这就是关键所在:编译时验证。如果要具备泛型的灵活性和类型安全性,则编译器必须了解您与之交互的泛型类型的大量信息。在许多情况下,您可能没有这些类的源代码,因此它必须从“某个地方”获取信息。 而且它确实获取到了:元数据。在与字节码并列的.class文件中嵌入了大量信息:编译器需要知道您如何安全地使用通用库类型的所有内容。那么哪些泛型信息得以保留?
类型变量和约束
编译器为了消费泛型类型需要知道的最基本的事情是类型变量的列表。对于任何通用类型或通用方法,类型变量的名称和位置都将被保留。此外,任何约束(上限或下限)也包括在内。
泛型超类型签名
有时候,您编写一个扩展通用类或实现通用接口的类。 如果您编写了一个StringList扩展了ArrayList,则会继承很多功能。如果有人想要“按预期”使用您的StringList而没有源代码,则仅知道您扩展了ArrayList是不够的;它必须知道您扩展了ArrayList。这递归地应用于层次结构:它必须知道ArrayList<>扩展了AbstractList<>等等。因此,这些信息得到保留,并且您的类文件将包括任何通用超类型(类或接口)的完整泛型签名。
成员签名
如果编译器不知道字段、方法参数和返回类型的完整泛型类型,通用类型的正确使用就无法得到验证。所以,你猜对了:该信息也被包含在内。如果类成员的任何部分包含通用类型、通配符或类型变量,则该成员将保存其签名信息。
局部变量
为了消费某种类型,不需要保留有关局部变量类型的信息。这可能有助于调试,但仅限于此。可以使用元数据表记录变量的名称和类型以及它们存在的字节码范围。取决于编译器,默认情况下它们可能被写入或者被省略。您可以通过传递-g:vars来强制javac发出它们,但我认为它们默认被省略。
调用站点
反编译器面临的最大问题之一,主要影响方法体中通用推断的是调用泛型方法的调用点不保留有关类型参数的任何信息。这给像 Java 8 Streams 这样的 API 带来了巨大的麻烦,其中泛型运算符被链接在一起,每个操作符都接受匿名类型的 lambda(其参数类型可能协变,返回类型可能逆变)。这是推理类型的噩梦,但对于与泛型交互的任何代码都是一个问题。这种代码只因存在于泛型类型内部而并不会变得更难反编译。
这如何影响反编译?
现代 Java 反编译器(如 Procyon 和 CFR)应该能够合理地重构泛型类型。如果本地变量元数据可用,结果应该非常接近原始代码。否则,他们将不得不尝试基于数据流分析来推断方法体中的泛型类型参数。本质上,反编译器必须查看流入和流出泛型实例化的数据,使用它所知道的关于该数据类型的信息来猜测类型参数。有时它表现得非常好;其他时候则不然(参见前面有关 Java 8 Streams 的注释)。
然而,在 API 级别 - 类型和成员签名方面,结果应该是完美无瑕的。
需要注意的是,严格来说,这里描述的所有元数据都是可选的:它只在编译时(或反编译时)需要。如果有人通过混淆器、优化器或其他实用程序运行他们的已编译类,则所有这些信息都可能被剥离。这在运行时不会有任何影响。
总之,如果所需的元数据存在,确实可以反编译带有类型参数的泛型类型和方法。在正确推断泛型实例和方法调用的类型参数方面可能比较棘手,但这是与泛型交互的任何代码都会遇到的问题。正如前面提到的,Procyon 和 CFR 应该都能很好地恢复泛型类型和方法。
ArrayList.class
并获取泛型类型ArrayList<T>
的源代码,包括T
的声明?或者,您是在问,当反编译一个包含变量List<String> myList
的方法时,您是否会在反编译的方法中看到变量被标记为List<String>
,而不仅仅是List
?这两种可能性截然不同。 - Mike Strobel<T>
? - OLIVER.KOO