为什么一个Java类用一个空行进行不同的编译?

为什么一个Java类用一个空行进行不同的编译?,java,compilation,javac,bytecode,Java,Compilation,Javac,Bytecode,我有以下Java类 public class HelloWorld { public static void main(String []args) { } } 当我编译这个文件并在生成的类文件上运行sha256时,我得到 9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff HelloWorld.class 接下来,我修改了该类并添加了一个空行,如下所示: public class HelloWorld {

我有以下Java类

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

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff HelloWorld.class
接下来,我修改了该类并添加了一个空行,如下所示:

public class HelloWorld {

  public static void main(String []args) {
  }
}
我再次在输出上运行了sha256,希望得到相同的结果,但是我得到了

11F7AD3AD03EB9E0BB7BFA3B97BBE0F17D31194D92CC683CFBD7852E2D189F HelloWorld.class
我读过:

只包含空格的行(可能带有注释)称为空行,Java完全忽略它

所以我的问题是,既然Java忽略了空行,为什么两个程序的编译字节码不同


也就是说,
HelloWorld.class
a
0x03
字节被替换为
0x04
字节的区别。

基本上,行号是为了调试而保留的,因此,如果您按原来的方式更改源代码,您的方法从不同的行开始,编译的类反映了差异。

您可以通过使用
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属性是代码属性(§4.7.3)属性表中的可选可变长度属性。调试器可以使用它来确定代码数组的哪个部分对应于原始源文件中的给定行号

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

在代码属性的属性表中,源文件的每行可能有多个LineNumberTable属性。也就是说,LineNumberTable属性可以一起表示源文件的给定行,而不必与源行一一对应


除了用于调试的任何行号详细信息外,清单还可以存储生成时间和日期。每次编译时,这自然会有所不同。

认为“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”
,不管换行的次数如何。

请注意,编译器在生成类文件时不必具有确定性,即使它们通常是确定性的。看见Jar文件在默认情况下是不可复制的,即即使编译相同的代码也会导致两个不同的Jar。这是因为文件的顺序和时间戳不匹配。可复制的构建可以通过特定的配置实现。TutorialsPoint声称“Java完全忽略”空行。但事实并非如此。相信哪一个?..@skomisa规范。@GiacomoAlzetta甚至没有一个字节码文件的指定字节码形式。例如,成员的顺序是未指定的,因此如果编译器在内部使用带有随机化的新不可变
s,则每次运行时可能会产生不同的顺序。它还可以添加一个包含编译时的自定义属性。等等…@DioPhung从中学到的另一个教训是:TutorialPoint不是优秀教程的可靠来源,这也解释了为什么OP报告的字节数不同:
传输结束
代表ASCII代码4,
文本结束
代表ASCII代码3为了实验证明这一点,我比较了编译时使用
-g:none
标志对OP源代码的文件进行分类(删除所有调试信息,请参阅),并在两种情况下获得相同的哈希值。为正式支持您的答案,请参阅以下第3.4节(“行终止符”):“Java编译器接下来通过识别行终止符将Unicode输入字符序列分成行……行终止符定义的行可以确定Java编译器生成的行号“。这些行号的一个重要用途是如果引发异常;它可以告诉您堆栈跟踪中异常的行号;直到最近,编译器总是在生成的程序集中嵌入一个新的GUID,这样就可以保证两个构建不会是二进制相同的,这样就可以区分它们了@EricLippert如果两个构建只在生成时间上不同(即相同的代码库),我们不应该将它们视为相同的吗?使用现代CI/CD构建管道(Jenkins、TeamCity、CircleCI),我们将有一种方法来区分构建,但从应用程序的角度来看,部署具有相同代码库的较新二进制文件似乎没有什么用处。@DioPhung相反。您不希望两个不同的构建具有相同的GUID,因为这是系统可以决定使用哪个构建的方式。所以每次生成一个新的GUID是最容易的;然后你会得到Eric描述为意外后果的副作用。@vikingsteve就像我说的,用相同的GUID报告两个不同的版本,然后作为相同的软件报告给系统,这将更加没有帮助。这将导致系统完全失效