问题 相同的Java源代码编译为二进制不同的类


任何人都可以解释相同的Java源代码最终可以编译到何处 二进制 不同的类文件?

问题来自以下情况:

我们有一个相当大的应用程序(800多个类)已经分支,重组然后重新集成回主干。在重新集成之前,我们将主干合并到分支中,这是标准过程。

最终结果是一组带有分支源的目录和一组带有主干源的目录。使用Beyond Compare,我们能够确定两组源相同。但是,在编译时(同样的JDK 使用IntelliJ v11中托管的maven)我们注意到大约十几个类文件是不同的。

当我们为每对明显不同的类文件反编译源代码时,我们最终得到了相同的java源代码,因此就最终结果而言,它似乎并不重要。但为什么只有少数文件不同?

谢谢。


额外的想法:

如果maven / javac以不同的顺序编译文件,可能会影响最终结果吗?


884
2017-10-01 11:16


起源

不同的jdk版本?我想不同版本的优化可能会有所不同。 - RNJ
使用javap -c -v(感谢Peter Lawrey)并使用Beyond Compare查看相应的输出(很棒的工具,喜欢它!)我可以确认Stephen C(感谢Stephen C)的第5项回答给出了部分答案。在一些情况下,常量池顺序是不同的。但是,我很确定两者的类路径是相同的,但编译顺序可能不同。 - Vicki


答案:


假设JDK版本,构建工具版本和构建/编译选项相同,我仍然可以想到5 可能 差异来源:

  1. 时间戳 - 类文件可以1 包含编译时间戳。除非您在完全相同的时间运行编译,否则同一文件的不同编译将导致不同的时间戳。

  2. 源文件名路径 - 每个类文件包含源文件的路径名。如果编译具有不同路径名的两个树,则类文件将包含不同的源路径名。

  3. 导入的编译时常量的值 - 当一个类时 A 使用另一个类中定义的编译时常量 B (有关“编译时常量”的定义,请参阅JLS),将常量的值合并到 As类文件。所以,如果你编译 A 针对不同版本的 B (具有不同的常量值),代码 A 可能会有所不同。

  4. 外部类/方法签名的差异;例如如果您在其中一个POM文件中更改了依赖项版本。

  5. 构建类路径的差异可能导致找到导入类的顺序不同,这可能导致类文件的常量池中条目顺序的非显着差异。这可能是由于以下因素:

    • 在外部JAR文件的目录中以不同顺序出现的文件,
    • 由于源文件在构建工具迭代时的顺序不同,文件按不同顺序编译,或者
    • 构建中的并行性(如果已启用)。

请注意,您通常不会看到FS目录中文件的实际顺序,因为工具类似 lsdir 默认在显示条目之前对条目进行排序。

1 - 这取决于编译器。此外,它不能保证 javap 将显示时间戳...如果它们存在。


我应该补充一点,确定差异原因的第一步是弄清楚它们究竟是什么。你可能需要(需要)以这种方式做到这一点 - 通过手动解码一对类文件来识别它们实际上存在差异的地方......以及这些差异实际意味着什么。


6
2017-10-01 12:24



斯蒂芬,我可以看到反编译中没有任何证据表明项目1,2和项目3和4在这里不适用(来源,包括POM,都是相同的记住)。但是,我认为第5项是可行的,因为编译器编译的序列在任何一种情况下都可能不同。 - Vicki
谢谢Stephen。我认为第5项及以后回答了我的问题。 - Vicki


当您使用超出比较进行比较时,将根据文件的内容进行比较。但是在构建过程中,只检查源文件的时间戳以进行更改。因此,您的源文件的最后修改日期更改将被重新编译。


2
2017-10-01 11:21



我应该澄清所有文件都是针对两组源编译的(即在编译之前没有存在类文件) - Vicki


不同的JDK产生不同的二进制类(优化,但也有类版本号)。还有编译选项(JDK可以用旧格式编译,或者它可以添加调试信息)。


1
2017-10-01 11:18



我已经编辑确认相同的JDK和编译选项等。 - Vicki


不同版本的Java可以添加不同的元数据,这些元数据通常会被反编译器忽略。

我建议你尝试使用 javap -c -v 了解文件中的更多详细信息。如果这没有帮助,您可以使用查看每个字节的ASMifierClassVisitor。


1
2017-10-01 11:20





同样的JDK也可以有不同的输出,具体取决于你的编译方式。 您可以使用或不使用调试信息进行编译,您可以编译为在旧版本中运行,每个选项都将导致其他类。


1
2017-10-01 11:22