Почему класс 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байт.

KNejad
источник
45
Обратите внимание, что компилятор не обязан быть детерминированным при создании файлов классов, даже если обычно они есть. Смотрите этот вопрос . Файлы JAR по умолчанию не воспроизводимы, то есть даже компиляция одного и того же кода приведет к двум разным JAR. Это связано с тем, что порядок файлов и метки времени не будут совпадать. Воспроизводимые сборки возможны с определенной конфигурацией.
Джакомо Альзетта
22
TutorialsPoint утверждает, что «Java полностью игнорирует» пустые строки. Раздел 3.4 Спецификации языка Java говорит об обратном. Кому верить? ...
Скомиса
37
@skomisa Спецификация.
wizzwizz4
4
@GiacomoAlzetta даже нет определенной формы байт-кода для одного файла байт-кода. Например, порядок членов не определен, поэтому, если компилятор использует новые неизменяемые значения Sets с рандомизацией внутри, он может создавать разные порядки при каждом запуске. Он также может добавить пользовательский атрибут, содержащий время компиляции. И так далее ...
Хольгер
15
@DioPhung извлек еще один урок: tutorialspoint не является надежным источником хороших учебных пособий
августа

Ответы:

331

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

Федерико Клез Каллока
источник
11
Это также объясняет, почему он отличается в байтах, сообщаемых OP: end-of-transmissionрасшифровывается как ASCII-код 4 и end-of-textобозначает ASCII-код 3
Ferrybig
160
Чтобы экспериментально доказать это, я сравнил хэши файлов классов исходного кода OP, используя -g:noneфлаг при компиляции (который удаляет всю отладочную информацию, см. Здесь ), и получил одинаковый хэш в обоих сценариях.
Капитан Мэн
14
В подтверждение вашего ответа из раздела 3.4 ( «Терминаторы строк» ) Спецификации языка Java для Java SE 11 : «Затем компилятор Java делит последовательность входных символов Unicode на строки, распознавая ограничители строк ... Определенные строки по ограничителям строки может определять номера строк, создаваемые компилятором Java " .
скомиса
4
Одним из важных применений этих номеров строк является исключение; он может сказать вам номер строки исключения в трассировке стека.
Гпаряни
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 на строку исходного файла в таблице атрибутов атрибута Code. То есть атрибуты 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
1
@xehpuk Единственный способ получить это -1- использовать -g:noneфлаг. Есть ли другой способ получить это исключение, используя обычные javac?
Андрей Тюкин
3
Я думаю, только с -gвозможностью. Есть также -g:varsи то, -g:sourceчто мешает генерации LineNumberTable.
xehpuk
14

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

Грэхем
источник
14
C # также имеет эту проблему; до недавнего времени компилятор всегда вставлял свежий GUID в сгенерированную сборку, чтобы вам было гарантировано, что две сборки не будут бинарно идентичными, чтобы вы могли отличить их друг от друга!
Эрик Липперт
3
@EricLippert, если две сборки отличаются только временем генерации (т. Е. Идентичной кодовой базой), разве мы не должны рассматривать их как одинаковые? С помощью современного конвейера сборки CI / CD (Jenkins, TeamCity, CircleCI) у нас будет возможность различать сборки, но с точки зрения приложения развертывание более новых двоичных файлов с идентичной базой кода не представляется полезным.
Дио Фунг
2
@DioPhung Это наоборот. Вы не хотите, чтобы две разные сборки имели одинаковый GUID, потому что именно так система может решить, какую из них использовать. Так что проще всего каждый раз генерировать новый GUID; и тогда вы получите побочный эффект, который Эрик описывает как непреднамеренное последствие.
Грэм
3
@vikingsteve Как я уже сказал, было бы еще менее полезно сообщать о двух разных сборках с одним и тем же GUID, который затем сообщался бы системе как одно и то же программное обеспечение. Это может привести к полному отказу любой схемы инициализации, поэтому крайне важно, чтобы идентификаторы GUID никогда не дублировались (с разумной вероятностью!). Наличие разных идентификаторов GUID для двух отдельных сборок одного и того же исходного кода - самое простое раздражение. Таким образом, перед лицом сценария критической неудачи то, что вы считаете немного бесполезным, на самом деле не имеет значения.
Грэм
4
@vikingsteve Кодовая часть бинарного файла все та же (если я понимаю, я не C # dev), это просто некоторые метаданные, которые прикреплены к бинарному файлу.
Капитан Мэн