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


207

我有以下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字节。


45
请注意,即使正常情况下编译器在生成类文件时也不必具有确定性。看到这个问题。默认情况下,Jar文件是不可复制的,即,即使编译相同的代码也会导致两个不同的JAR。这是因为文件的顺序和时间戳不匹配。通过特定配置,可以实现可复制的构建。
Giacomo Alzetta

22
TutorialsPoint声称“ Java完全忽略”了空行。Java语言规范的第3.4节另有规定。相信哪一个?...
Skomisa

37
@skomisa规范。
wizzwizz4 '18 -10-3

4
@GiacomoAlzetta甚至没有单个字节码文件的指定字节码形式。例如,成员的顺序未指定,因此,如果编译器在Set内部使用具有随机化功能的新的不可变,则每次运行可能会产生不同的顺序。它还可以添加包含编译时的自定义属性。等等…
Holger

15
@DioPhung吸取了另一个教训:tutorialspoint并不是可靠的优秀教程的来源
jwenting

Answers:


331

基本上,行号是为了调试而保留的,因此,如果以这种方式更改源代码,则方法将从另一行开始,并且编译后的类会反映出差异。


11
这也解释了为什么它在OP报告的字节中有所不同:end-of-transmission代表ASCII代码4和end-of-text代表ASCII代码3
Ferrybig '18

160
为了实验证明这一点,我-g:none在编译时使用了标志比较了OP源文件的类文件的哈希值(删除了所有调试信息,请参见此处),并且在两种情况下都得到了相同的哈希值。
曼队长

14
为了对您的答案提供正式支持,请参见Java SE 11 Java语言规范的第3.4节(“行终止):“接下来,Java编译器通过识别行终止符将Unicode输入字符的序列划分为行... 定义行由行结束可以确定由Java编译器产生的行号
斯科米萨

4
这些行号的一个重要用途是抛出异常。它可以告诉您堆栈跟踪中异常的行号。
gparyani

114

您可以使用来查看更改,该更改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属性,则它们可以按任何顺序出现。

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


57

“ 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 '18 -10-5

1
@xehpuk我唯一能得到的方法-1是使用该-g:none标志。还有其他使用普通方法获取此异常的方法javac吗?
安德烈·秋金

3
我猜只有-g选择。也存在,-g:vars并且-g:source会阻止的生成LineNumberTable
xehpuk '18 -10-5

14

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


14
C#也有这个问题。直到最近,编译器始终将新的GUID嵌入到生成的程序集中,这样可以保证两个构建不会是二进制相同的,因此可以区分它们!
埃里克·利珀特

3
@EricLippert如果两个版本的生成时间不同(即相同的代码库),我们是否应该将它们视为相同?借助现代的CI / CD构建管道(Jenkins,TeamCity,CircleCI),我们将能够区分构建,但是从应用程序的角度来看,使用相同代码库部署更新的二进制文件似乎没有用。
Dio Phung

2
@DioPhung是另一种方式。您不希望两个不同的版本具有相同的GUID,因为这是系统可以决定使用哪个版本的方式。因此,最简单的方法是每次生成一个新的GUID。然后您会得到Eric描述的意外后果。
格雷厄姆

3
@vikingsteve就像我说的那样,使用相同的GUID报告两个不同的版本将毫无帮助,然后将它们作为相同的软件报告给系统。这将导致任何种类的配置方案完全失败,因此,绝不重复GUID(在合理的概率内!)是至关重要的。对于相同源代码的两个独立版本,具有不同的GUID最多是一件小事。因此,面对关键任务失败的情况,您认为略微无助的事情实际上是没有用的。
格雷厄姆

4
@vikingsteve 二进制文件的代码部分仍然是相同的(如果我理解的话,我不是C#开发人员),只是附加到二进制文件的一些元数据。
曼队长
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.