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

KNe*_*jad 206 java bytecode compilation javac

我有以下Java类

public class HelloWorld {
  public static void main(String []args) {
  }
}
Run Code Online (Sandbox Code Playgroud)

当我编译这个文件并在生成的类文件上运行sha256时,我得到了

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class
Run Code Online (Sandbox Code Playgroud)

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

public class HelloWorld {

  public static void main(String []args) {
  }
}
Run Code Online (Sandbox Code Playgroud)

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

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class
Run Code Online (Sandbox Code Playgroud)

我已阅读此TutorialsPoint文章:

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

所以我的问题是,由于Java忽略空行,为什么两个程序的编译字节码都不同?

即在该差HelloWorld.class一个0x03字节是由替换0x04字节.

Fed*_*oca 328

基本上,行号保留用于调试,因此如果您按照您的方式更改源代码,则您的方法从不同的行开始,编译的类反映了差异.

  • 为了实验证明这一点,我在编译时使用`-g:none`标志比较了OP源代码文件的哈希值(删除了所有调试信息,请参见[here](https://docs.oracle.com/javase/) 8/docs/technotes/tools/windows/javac.html))并在两种情况下获得相同的哈希值. (158认同)
  • 在正式支持您的答案时,请参见[Java Java 11 Java语言规范]的第3.4节(_"Line Terminators"_)(https://docs.oracle.com/javase/specs/jls/se11/jls11. pdf):_"Java编译器接下来通过识别行终止符将Unicode输入字符序列划分为行...行终止符定义的**行可以确定Java编译器产生的行号**"_. (14认同)
  • 这也解释了为什么它在OP报告的字节数上有所不同:`传输结束'代表ASCII代码4,`end-of-text`代表ASCII代码3 (11认同)
  • 这些行号的一个重要用途是抛出异常; 它可以告诉你堆栈跟踪中异常的行号. (3认同)

Kar*_*cki 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:
Run Code Online (Sandbox Code Playgroud)

更确切地说,类文件在以下LineNumberTable部分中有所不同:

LineNumberTable属性是Code属性(第4.7.3节)的attributes表中的可选variable-length属性.调试器可以使用它来确定代码数组的哪个部分对应于原始源文件中的给定行号.

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

在Code属性的attributes表中,每行源文件可能有多个LineNumberTable属性.也就是说,LineNumberTable属性可以一起表示源文件的给定行,并且不需要与源行一对一.


And*_*kin 53

"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]);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

这适用于可以访问具有行号的堆栈帧的每种语言,而不仅仅是Java.

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

  • 它还可以在线程"main"java.lang.ArrayIndexOutOfBoundsException:-1`中打印`Exception. (11认同)
  • 我想只有`-g`选项.还有`-g:vars`和`-g:source`,它们阻止了`LineNumberTable`的生成. (3认同)

Gra*_*ham 13

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

  • C#也有这个问题; 直到最近,编译器总是在生成的程序集中嵌入一个新的GUID,这样才能保证两个构建将*不是二进制相同,这样你就可以区分它们了! (14认同)
  • @vikingsteve二进制文件的*code*部分仍然是相同的(如果我理解,我不是C#dev),它只是附加到二进制文件的一些元数据. (4认同)
  • @EricLippert如果两个构建只是它们生成的时间不同(即相同的代码库),我们不应该将它们视为相同吗?使用现代CI/CD构建管道(Jenkins,TeamCity,CircleCI),我们将有一种方法来区分构建,但从应用程序的角度来看,部署具有相同代码库的较新二进制文件似乎没有用处. (3认同)
  • @vikingsteve就像我说的那样,使用相同的GUID报告两个不同的版本会更有帮助,然后将其作为相同的软件报告给系统.这将导致任何类型的供应方案完全失败,因此GUID的任务至关重要,永远不会重复(在合理的概率内!).对于相同源代码的两个单独构建具有不同的GUID最多是微不足道的烦恼.因此,面对关键任务失败的情况,你认为有点无益的东西真的无法理解. (3认同)
  • @DioPhung这是另一种方式.您不希望两个*不同的*版本具有相同的GUID,因为这是系统如何决定使用哪个.因此,每次生成新的GUID最简单; 然后你得到埃里克描述的副作用是一种意想不到的后果. (2认同)