为什么Java类在有空行的情况下编译结果不同?

212

我有以下的Java类

public class HelloWorld {
  public static void main(String []args) {
  }
}
当我编译这个文件并在生成的类文件上运行sha256时,我得到:
9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

接下来我修改了这个类,添加了一个空白行,如下所示:

public class HelloWorld {

  public static void main(String []args) {
  }
}

我再次对输出结果运行了sha256,期望得到相同的结果,但是实际上我得到了

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

我在这篇TutorialsPoint文章上看到:

一个只包含空格,可能带有注释的行被称为空白行,Java会完全忽略它。

因此我的问题是,既然Java忽略空白行,为什么两个程序的编译后的字节码不同呢?

换句话说,在HelloWorld.class中,一个0x03字节被替换为0x04字节。


47
请注意,编译器在生成类文件时没有必要是确定性的,尽管通常它们是确定性的。参见此问题。默认情况下,JAR文件是可再现的,即使编译相同的代码也会产生两个不同的JAR包。这是因为文件的顺序和时间戳不匹配。可以通过特定配置实现可再现构建。 - Giacomo Alzetta
22
TutorialsPoint声称Java会完全忽略空白行,但是Java语言规范的第3.4节却有所不同。应该相信哪一个呢? - skomisa
39
规格说明。 - wizzwizz4
4
@GiacomoAlzetta,甚至没有为单个字节码文件指定特定的字节码形式。例如,成员的顺序是未指定的,因此如果编译器在内部使用具有随机化的新不可变“Set”,它可能会在每次运行时产生不同的顺序。它还可以添加一个包含编译时间的自定义属性等等... - Holger
16
@DioPhung 又学到了一课:tutorialspoint 不是一个可靠的好教程来源。 - jwenting
显示剩余2条评论
4个回答

339

基本上,行号是为了调试而保留的,因此如果您像您所做的那样更改源代码,您的方法将从不同的行开始,并且编译后的类反映了这种差异。


11
这也解释了为什么OP报告的字节不同:end-of-transmission代表ASCII码4,而end-of-text代表ASCII码3。 - Ferrybig
163
为了实验性地证明这一点,我比较了使用 -g:none 标志编译 OP 源代码时类文件的哈希值(该标志会删除所有调试信息,请参见此处),并且在两种情况下得到了相同的哈希值。 - Captain Man
14
为了正式支持你的答案,我引用了Java SE 11版本的《Java语言规范》中3.4节“行终止符”的内容:“Java编译器通过识别行终止符将Unicode输入字符序列分成行。由行终止符所定义的行可能会决定Java编译器生成的行号。” - skomisa
5
这些行号的一个重要用途是当抛出异常时,它可以在堆栈跟踪中告诉你异常发生的行号。 - gparyani

116
你可以通过使用 javap -v 命令来查看变化,它会输出详细信息。正如其他人已经提到的那样,差异将在行号中体现。

javap -v 命令可以展示详细信息,可以通过它来查看变化。就像其他人已经提到的一样,区别将在行号上显示:

$ javap -v HelloWorld.class > with-line.txt
$ javap -v HelloWorld.class > no-line.txt
$ diff -C 1 no-line.txt with-line.txt
*** no-line.txt 2018-10-03 11:43:32.719400000 +0100
--- with-line.txt       2018-10-03 11:43:04.378500000 +0100
***************
*** 2,4 ****
    Last modified 03-Oct-2018; size 373 bytes
!   MD5 checksum 058baea07fb787bdd81c3fb3f9c586bc
    Compiled from "HelloWorld.java"
--- 2,4 ----
    Last modified 03-Oct-2018; size 373 bytes
!   MD5 checksum 435dbce605c21f84dda48de1a76e961f
    Compiled from "HelloWorld.java"
***************
*** 50,52 ****
        LineNumberTable:
!         line 3: 0
        LocalVariableTable:
--- 50,52 ----
        LineNumberTable:
!         line 4: 0
        LocalVariableTable:

更准确地说,类文件在 LineNumberTable 部分有所不同:

LineNumberTable 属性是 Code 属性(§4.7.3)的属性表中的可选变长属性。它可被调试器用于确定代码数组的哪部分对应原始源文件中的某个行号。

如果 Code 属性的属性表中存在多个 LineNumberTable 属性,则它们可以以任意顺序出现。

在 Code 属性的属性表中,每行源文件中可能有多个 LineNumberTable 属性。也就是说,LineNumberTable 属性可以共同表示源文件的某一行,并且不必与源行一一对应。


60

"Java忽略空白行"的假设是错误的。这里有一段代码片段,它在方法main之前的空行数量不同的情况下表现出不同的行为:

class NewlineDependent {

  public static void main(String[] args) {
    int i = Thread.currentThread().getStackTrace()[1].getLineNumber();
    System.out.println((new String[]{"foo", "bar"})[((i % 2) + 2) % 2]);
  }
}

如果在main之前没有空行,则会打印"foo",但如果在main之前有一个空行,则会打印"bar"

由于运行时行为不同,.class文件必须不同,无论时间戳或其他元数据如何。

对于每种可以访问具有行号的堆栈帧的语言,都适用此规则,而不仅仅是Java。

注意:如果使用-g:none(没有任何调试信息)编译,则不会包括行号,getLineNumber()始终返回-1,程序始终打印"bar",无论换行符的数量。


11
它还可以打印出Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1 - xehpuk
1
@xehpuk 我唯一能得到“-1”的方法是使用“-g:none”标志。是否有其他方法可以使用普通的javac来获取此异常? - Andrey Tyukin
3
我猜只有使用-g选项。还有-g:vars-g:source可以防止生成LineNumberTable - xehpuk

14

除了调试所需的任何行号细节外,你的清单文件还可以存储构建时间和日期。这将在每次编译时自然地不同。


14
C# 也存在这个问题;直到最近,编译器总是将一个全新的 GUID 嵌入生成的程序集中,这样你可以保证两次构建不会是二进制相同的,因此你可以将它们区分开来! - Eric Lippert
3
如果两个构建仅因生成时间不同而不同(即代码库相同),我们是否应将它们视为相同?使用现代 CI/CD 构建流水线(如 Jenkins、TeamCity、CircleCI),我们有一种区分构建的方法,但从应用程序的角度来看,使用相同代码库的新二进制文件似乎并不有用。 - Dio Phung
2
@DioPhung,恰恰相反。您不希望两个不同的构建具有相同的GUID,因为这就是系统决定使用哪一个的方式。因此,每次生成新的GUID最容易;然后您会得到Eric所描述的副作用,这是一个意外的结果。 - Graham
1
我认为对于本质上相同的构建而言,产生两个不同的二进制文件并不是有益的。元信息应该保持不变,就我个人而言。 - vikingsteve
3
就像我之前所说的,如果两个不同版本的软件使用相同的GUID进行报告,这将会更加无益,因为系统会将它们视为相同的软件。这将导致任何类型的配置方案完全失败,因此绝不能重复使用GUID(在合理的概率范围内)。对于同一源代码的两个不同构建来说,拥有不同的GUID只是一个微不足道的困扰。因此,在面对关键任务失败的情况下,您认为的稍微不利实际上并不起作用。 - Graham
4
@vikingsteve说的是,二进制代码部分仍然保持不变(如果我理解正确的话,我不是C#开发人员),只是一些元数据附加在二进制文件上。 - Captain Man

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接