是否可以将Java字节码反编译回原始泛型类型参数

OLI*_*KOO 7 java generics bytecode decompiler

我知道Java编译器用泛型替换泛型类型中的所有类型参数,或者Object在类型擦除过程中类型参数是否无界限.生成的机器字节码将反映被替换的边界或Object.

有没有办法获取生成的机器字节码并将其反编译回包含泛型类型中的原始类型参数的Java文件?是否存在可以实现此目的的反编译器?或者由于编译过程的性质,这个过程是不可逆转的?

Mik*_*bel 5

您是对的,在字节码级别,当您定义泛型类型并与之交互时,会丢失很多信息。类型擦除对于保持兼容性很好:如果您主要在编译时强制执行类型安全,则在运行时不需要做太多事情,因此您可以将泛型类型减少到它们的“原始”等价物。

这就是关键:编译时验证。如果您想要泛型的灵活性和类型安全性,您的编译器必须非常了解您与之交互的泛型类型。在许多情况下,您没有这些类的源代码,因此它必须从某个地方获取信息。它确实:元数据。.class与字节码一起嵌入文件中的是丰富的信息:编译器需要知道您正在安全地使用泛型库类型的所有信息。那么什么样的泛型信息会被保留下来呢?

类型变量和约束

为了使用泛型类型,编译器需要知道的最基本的事情是类型变量列表。对于任何泛型类型或泛型方法,都会保留类型变量的名称和位置。此外,还包括任何约束(上限或下限)。

通用超类型签名

有时您会编写一个扩展泛型类或实现泛型接口的类。如果你写了一个StringListextends ArrayList<String>,你继承了很多功能。如果有人想在没有源代码的情况下StringList 按预期使用您的代码,编译器仅知道您扩展了ArrayList; 它必须知道你扩展ArrayList<String>。这在层次结构中向上传递:它必须知道ArrayList<>extends AbstractList<>,等等。所以这些信息被保留了下来。您的类文件 a 将包含任何泛型超类型(类或接口)的完整泛型签名。

会员签名

如果编译器不知道字段、方法参数和返回类型的完整泛型类型,则它无法验证您是否正确使用了泛型类型。所以,你猜对了:这些信息被包括在内。如果类成员的任何部分包含泛型类型、通配符或类型变量,则该成员将获取其保存在元数据中的签名信息。

局部变量

没有必要为了使用类型而保留有关局部变量类型的信息。它对调试很有用,但仅此而已。有一些元数据表可用于记录变量的名称和类型,以及它们存在的字节码范围。根据编译器的不同,它们可能会或可能不会默认编写。您可以javac通过传递强制发出它们-g:vars,但我相信默认情况下它们会被省略

呼叫站点

反编译器的最大问题之一,主要影响方法主体内的泛型推理,是调用泛型方法的调用站点保留有关类型参数的信息。这给像 Java 8 Streams 这样的 API 带来了巨大的麻烦,在这些 API 中,泛型运算符被链接在一起,每个运算符都接受匿名类型的 lambdas(它们的参数类型可能是逆变的,而返回类型可能是协变的)。这是类型推断的噩梦,但对于碰巧泛型交互的任何代码来说,这都是一个问题。这种代码不会因为它存在泛型类型中而变得更难反编译。

这如何影响反编译

像 Procyon 和 CFR 这样的现代 Java 反编译器应该能够相当好地重构泛型类型。如果局部变量元数据可用,结果应该与原始代码非常接近。如果没有,他们将不得不尝试根据数据流分析推断方法主体中的泛型类型参数。本质上,反编译器必须查看哪些数据流入和流出泛型实例,并使用它对数据类型的了解来猜测类型参数。有时效果很好;其他时候,没有那么多(参见之前关于 Java 8 Streams 的评论)。

但是,在 API 级别——类型和成员签名——结果应该是准确的。

注意事项

严格来说,这里描述的所有元数据都是可选的:它只在编译时(或反编译时)需要。如果有人通过混淆器、优化器或其他一些实用程序运行他们编译的类,所有这些信息都可能被剥离。它不会在运行时产生影响。

tldr; 结论

是的,当然可以在类型参数完整的情况下反编译泛型类型和方法。假设存在所需的元数据,正确获取类型和成员签名是“容易”的部分。正确推断泛型实例和方法调用的类型参数是一个棘手的问题,但这对于碰巧与泛型交互的任何代码来说都是一个问题。

如前所述,Procyon 和 CFR 都应该在恢复泛型类型和方法方面做得相当不错。