Почему класс Java компилируется по-разному с пустой строкой?

У меня есть следующий класс 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.

Ответ 1

В основном номера строк сохраняются для отладки, поэтому, если вы изменили исходный код так, как вы это делали, ваш метод начинается с другой строки, и скомпилированный класс отражает разницу.

Ответ 2

Вы можете увидеть изменение с помощью 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 в таблице атрибутов атрибута Code. То есть атрибуты LineNumberTable могут вместе представлять заданную строку исходного файла и не должны быть взаимно однозначными с исходными строками.

Ответ 3

Предположение о том, что "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", независимо от количества разрывы строк.

Ответ 4

Как и любые номера строк для отладки, ваш манифест также может хранить время и дату сборки. Естественно, это будет каждый раз при компиляции.