1.5 反编译Scala代码

问题

在你学习Scala代码如何被编译成类文件或者试图理解某个特定问题的过程中,你有可能会想要检查由Scala编译器从源代码生成的字节码。

解决方案

反编译Scala代码的主要方法是使用javap命令,也可以使用反编译器将类文件转换回Java源代码,这种方式将会在后面讨论。

因为Scala源代码被编译成了普通的JVM类文件,所以可以使用javap命令来反编译它们。比如,假设有一个名为Person.scala的文件,它包含的代码如下所示:

下一步,使用scalac编译该文件:

现在可以像这样,使用javap反编译生成的Person.class文件,下面输出的是它的签名:

以上代码显示了Person类的公共签名,也就是它的公共API或者接口。即使是一个这么简单的例子,也可以从中看出Scala编译器所做的工作:创建了name()、name_$eq、age()和age_$eq等方法。在下面的讨论中会有更详细的例子。

可以使用javap-private选项查看额外的信息:

javap还有几个有用的选项。使用-c选项可以看到组成Java字节码的实际命令,再加上-verbose选项可以看到更多细节。要了解所有选项的细节,请运行javap-help。

讨论

了解Scala工作方式的一种有效方法是用javap反编译类文件来获取相关信息。正如第一个例子里的Person类,将构造函数的参数name和age定义为var字段,可以生成很多方法。

作为第二个例子,把这两个字段的var属性去掉将得到以下定义:

用scalac编译这个类,然后在生成的类文件上运行javap,会得到一个更短的类签名:

相反,在两个字段上都留下var,并把这个类变成一个样例类,那么会大大增加Scala生成的代码量。为了看到这一点,只要像下面这样修改Person.scala,你就会得到这个样例类:

编译这段代码会创建两个输出文件——Person.classPerson$.class。使用javap来反编译这两个文件会得到:

如前所示,当把一个类定义成样例类时,Scala会生成大量代码,上面的输出显示了这些代码的公共签名。关于这段代码的详细讨论,请参阅5.14节。

关于.tasty文件

细心的你可能已经注意到了,除了.class文件之外,Scala 3在编译过程中还会生成.tasty文件。这些文件是以TASTy格式生成的,而TASTy这个缩写来自类型化抽象语法树(typed abstract syntax tree)这个术语。

TASTy检查文档(https://oreil.ly/VHJj2)指出:“TASTy文件包含一个类的完整类型树,包括源代码和文档。这对于分析或从代码中提取语义信息的工具来说是非常理想的。”

它的用途之一是用于Scala 3和Scala 2.13+之间的集成。正如“Scala向前兼容”(https://oreil.ly/P4joi)所说,“Scala 2.13可以读取这些(TASTy)文件来了解哪些术语、类型和隐式是在给定的依赖中定义的,以及需要生成哪些代码来正确使用它。编译器中管理这部分的功能被称为Tasty Reader。”

另见

·在“How to Create Inline Methods in Scala 3”(https://oreil.ly/Ewec8)这篇博客中,我展示了如何使用这种技术来理解inline方法。

·也可以使用反编译器将.class文件反编译成Java代码。我曾使用过一个名为JAD的工具,但它在2001年就停止更新了。令人惊讶的是,20年后它还能对类文件进行部分反编译。在Scala Gitter频道(https://oreil.ly/4arCQ)中也提到过一个名为CFR(https://oreil.ly/mjSMp)的更现代的反编译器。

更多关于TASTy和.tasty文件的资料如下:

·“Macros:the Plan for Scala 3”(https://oreil.ly/HyQUi

·“Forward Compatibility for the Scala 3 Transition”(https://oreil.ly/P4joi

·“Scala 3 Migration Guide:Compatibility Reference”(https://oreil.ly/7AXv8