Чому клас 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. Це тому, що порядок файлів і часові позначки не збігаються. Можливі відтворювані збірки з певною конфігурацією.
Джакомо Альзетта

22
TutorialsPoint стверджує, що "Java повністю ігнорує" порожні рядки. Розділ 3.4 Спеціалізації мови Java говорить про інше. У кого вірити? ...
skomisa

37
@skomisa Специфікація.
wizzwizz4

4
@GiacomoAlzetta не існує навіть визначеної форми байт-коду для одного файлу байт-коду. Наприклад, порядок членів не визначений, тому якщо компілятор використовує нові незмінні Sets з рандомізацією внутрішньо, він може створювати різний порядок на кожному запуску. Він також може додати спеціальний атрибут, що містить час компіляції. І так далі…
Холгер

15
@DioPhung ще один засвоєний урок: уроки не є надійним джерелом для хороших навчальних посібників
jwenting

Відповіді:


331

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


11
Це також пояснює, чому вона відрізняється в байтах, про які повідомляє ОП: end-of-transmissionрозшифровується код ASCII 4 і end-of-textозначає код ASCII 3
Феррібіг

160
Щоб експериментально довести це, я порівняв хеші файлів класів джерела OP, використовуючи -g:noneпрапор при компілюванні (який видаляє всю інформацію про налагодження, дивіться тут ) і отримав однаковий хеш в обох сценаріях.
Капітан Людина

14
У формальній підтримці вашої відповіді з розділу 3.4 ( "Термінали ліній" ) Специфікації мови Java для Java SE 11 : "Компілятор Java далі розділяє послідовність символів введення Unicode на рядки шляхом розпізнавання термінальних рядків ... Рядки визначені термінальні лінії можуть визначати номери рядків, створені компілятором Java " .
skomisa

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). Він може використовуватись налагоджувачами для визначення того, яка частина масиву коду відповідає заданому номеру рядка у вихідному файлі джерела.

Якщо декілька атрибутів LineNumberTable є в таблиці атрибутів атрибута Code, вони можуть відображатися в будь-якому порядку.

У таблиці атрибутів атрибута Code може бути більше одного атрибута 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

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), це лише деякі метадані, які додаються до двійкових.
Капітан Людина
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.