你是正确的,在字节码级别,当您定义泛型类型并与其进行交互时,会丢失很多信息.类型擦除对于保持兼容性很好:如果你在编译时主要强制类型安全,那么你不需要在运行时做很多事情,所以你可以将泛型类型减少到它们的“原始”等价物.
这就是关键:编译时验证.如果您想要泛型的灵活性和类型安全性,您的编译器必须了解与您交互的泛型类型.在许多情况下,您将没有这些类的源代码,因此必须从某处获取信息.它确实:元数据.嵌入.class文件和字节码旁边是丰富的信息:编译器需要知道您安全使用通用库类型的所有内容.那么什么样的仿制药信息得以保存?
输入变量和约束
编译器为了使用泛型类型而需要知道的最基本的事情是类型变量列表.对于任何泛型类型或泛型方法,都会保留类型变量的名称和位置.此外,还包括任何约束(上限或下限).
通用超类型签名
有时您编写一个扩展泛型类或实现泛型接口的类.如果编写扩展ArrayList< String>的StringList,则会继承许多功能.如果有人想按预期使用你的StringList而没有源代码,那么编译器就不足以知道扩展了ArrayList;它必须知道扩展的ArrayList< String>.这适用于层次结构:它必须知道ArrayList<>扩展AbstractList<>,等等.所以这些信息得以保留.您的类文件a将包含任何通用超类型(类或接口)的完整通用签名.
会员签名
如果编译器不知道完整的通用类型的字段,方法参数和返回类型,则无法验证您是否正确使用了泛型类型.所以,你猜对了:信息包括在内.如果类成员的任何部分包含泛型类型,通配符或类型变量,则该成员将获取其签名信息保存在元数据中.
局部变量
为了使用类型,没有必要保留有关局部变量类型的信息.它可以用于调试,但就是这样.有一些元数据表可用于记录变量的名称和类型,以及它们存在的字节码范围.根据编译器的不同,默认情况下可能会编写也可能不编写它们.您可以通过传递-g:vars强制javac发出它们,但我相信它们在默认情况下会被省略
呼叫网站
反编译器的最大问题之一,主要是影响方法体内的泛型推理,是调用泛型方法的调用站点不保留有关类型参数的信息.这给像Java 8 Streams这样的API带来了巨大的麻烦,在这些API中,泛型运算符被链接在一起,每个都接受匿名类型的lambdas(在它们的参数类型中可能是逆变的,在返回类型中可能是协变的).这是一种类型的推理噩梦,但对于碰巧与泛型交互的任何代码来说都是一个问题.这种代码不会因为它存在于泛型类型中而变得非常难以反编译.
这会如何影响反编译
像Procyon和CFR这样的现代Java反编译器应该能够很好地重建泛型类型.如果局部变量元数据可用,则结果应该非常接近原始代码.如果没有,他们将不得不尝试基于数据流分析推断方法体中的泛型类型参数.本质上,反编译器必须查看哪些数据流入和流出泛型实例,并使用它所知道的数据类型来猜测类型参数.有时候效果很好;其他时候,并没有那么多(参见之前关于Java 8 Streams的评论).
在API级别,虽然类型和成员签名 – 结果应该是定点.
注意事项
严格地说,这里描述的所有元数据都是可选的:它只在编译时(或反编译时)需要.如果有人通过混淆器,优化器或其他实用程序运行其编译的类,则所有这些信息都可能被删除.它在运行时不会有所作为.
tldr;结论
是的,当然可以使用其类型参数完整地反编译泛型类型和方法.假设存在所需的元数据,那么获得正确的类型和成员签名就是“简单”部分.正确地推断泛型实例和方法调用的类型参数是棘手的,但这对于碰巧与泛型交互的任何代码来说都是一个问题.
如前所述,Procyon和CFR都应该在恢复泛型类型和方法方面做得相当不错.