Dlaczego klasa Java kompiluje się inaczej z pustą linią?

207

Mam następującą klasę Java

public class HelloWorld {
  public static void main(String []args) {
  }
}

Kiedy kompiluję ten plik i uruchamiam sha256 na wynikowym pliku klasy, otrzymuję

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Następnie zmodyfikowałem klasę i dodałem pusty wiersz w następujący sposób:

public class HelloWorld {

  public static void main(String []args) {
  }
}

Znów uruchomiłem sha256 na wyjściu, spodziewając się tego samego rezultatu, ale zamiast tego dostałem

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

Przeczytałem o tym artykule z TutorialsPoint, który:

Linia zawierająca tylko białe znaki, prawdopodobnie z komentarzem, jest znana jako pusta linia, a Java całkowicie ją ignoruje.

Moje pytanie brzmi: skoro Java ignoruje puste wiersze, dlaczego skompilowany kod bajtowy jest inny dla obu programów?

Mianowicie różnica, że HelloWorld.classw 0x03bajcie jest zastąpiony 0x04bajt.

KNejad
źródło
45
Zauważ, że kompilator nie jest zobowiązany do deterministycznego tworzenia plików klas, nawet jeśli zwykle tak jest. Zobacz to pytanie . Pliki jar domyślnie nie są odtwarzalne, tzn. Nawet skompilowanie tego samego kodu spowoduje utworzenie dwóch różnych plików JAR. Jest tak, ponieważ kolejność plików i znaczniki czasu nie będą się zgadzać. Możliwe są powtarzalne kompilacje przy określonej konfiguracji.
Giacomo Alzetta
22
TutorialsPoint twierdzi, że „Java całkowicie ignoruje” puste linie. Sekcja 3.4 specyfikacji języka Java mówi inaczej. Który uwierzyć? ...
skomisa
37
@skomisa Specyfikacja.
wizzwizz4
4
@GiacomoAlzetta nie ma nawet określonej formy kodu bajtowego dla pojedynczego pliku kodu bajtowego. Na przykład kolejność elementów jest nieokreślona, ​​więc jeśli kompilator używa nowych niezmiennych Setz losową funkcją wewnętrzną, może generować inną kolejność przy każdym uruchomieniu. Może także dodać niestandardowy atrybut zawierający czas kompilacji. I tak dalej…
Holger,
15
@DioPhung wyciągnięto kolejną lekcję: tutorialspoint nie jest wiarygodnym źródłem dobrych tutoriali
jwenting

Odpowiedzi:

331

Zasadniczo numery wierszy są przechowywane w celu debugowania, więc jeśli zmienisz kod źródłowy w ten sam sposób, twoja metoda rozpocznie się od innego wiersza, a skompilowana klasa odzwierciedla różnicę.

Federico klez Culloca
źródło
11
To wyjaśnia również, dlaczego różni się w bajtach zgłoszonych przez OP: end-of-transmissionoznacza kod ASCII 4 i end-of-textoznacza kod ASCII 3
Ferrybig
160
Aby eksperymentalnie to udowodnić, porównałem skróty plików klasowych źródła OP za pomocą -g:noneflagi podczas kompilacji (która usuwa wszystkie informacje debugowania, patrz tutaj ) i uzyskałem ten sam skrót w obu scenariuszach.
Captain Man,
14
W formalnym wsparciu dla twojej odpowiedzi, z sekcji 3.4 ( „Terminatory linii” ) specyfikacji języka Java dla Java SE 11 : „Kompilator Java następnie dzieli sekwencję znaków wejściowych Unicode na linie, rozpoznając terminatory linii ... Linie zdefiniowane przez terminatory linii mogą określać numery linii generowane przez kompilator Java .
skomisa
4
Jednym z ważnych zastosowań tych numerów linii jest zgłoszenie wyjątku; może podać numer linii wyjątku w śladzie stosu.
gparyani
114

Możesz zobaczyć zmianę za pomocą, javap -vktóra wyświetli pełne informacje. Podobnie jak inne wcześniej wspomniane, różnica będzie podana w numerach wierszy:

$ 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:

Dokładniej plik klasy różni się w LineNumberTablesekcji:

Atrybut LineNumberTable jest opcjonalnym atrybutem o zmiennej długości w tabeli atrybutów atrybutu Code (pkt 4.7.3). Może być wykorzystywany przez debuggery do określania, która część tablicy kodów odpowiada danemu numerowi wiersza w oryginalnym pliku źródłowym.

Jeśli w tabeli atrybutów atrybutu Code występuje wiele atrybutów LineNumberTable, mogą one pojawiać się w dowolnej kolejności.

W tabeli atrybutów atrybutu Code może znajdować się więcej niż jeden atrybut LineNumberTable w wierszu pliku źródłowego. Oznacza to, że atrybuty LineNumberTable mogą razem reprezentować daną linię pliku źródłowego i nie muszą być jeden-do-jednego z liniami źródłowymi.

Karol Dowbecki
źródło
57

Założenie, że „Java ignoruje puste linie” jest błędne. Oto fragment kodu, który zachowuje się inaczej w zależności od liczby pustych linii przed metodą 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]);
  }
}

Jeśli wcześniej nie było żadnych pustych linii main, drukuje "foo", ale z jedną pustą linią wcześniej main, drukuje "bar".

Ponieważ zachowanie w czasie wykonywania jest inne, .classpliki muszą być różne, niezależnie od znaczników czasu i innych metadanych.

Dotyczy to każdego języka, który ma dostęp do ramek stosu z numerami linii, nie tylko w języku Java.

Uwaga: jeśli jest skompilowany -g:none(bez żadnych informacji debugujących), wówczas numery linii nie zostaną uwzględnione, getLineNumber()zawsze zwraca -1, a program zawsze drukuje "bar", niezależnie od liczby podziałów linii.

Andrey Tyukin
źródło
11
Może także drukować Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk
1
@xehpuk Jedynym sposobem, w jaki mogłem uzyskać, -1było użycie -g:noneflagi. Czy istnieje inny sposób uzyskania tego wyjątku za pomocą zwykłego javac?
Andrey Tyukin
3
Chyba tylko z -gopcją. Jest też -g:varsi -g:sourcektóry zapobiega generowaniu LineNumberTable.
xehpuk
14

Oprócz szczegółów numeru linii do debugowania, manifest może także przechowywać datę i godzinę kompilacji. Będzie to naturalnie różnić się przy każdym kompilacji.

Graham
źródło
14
C # ma również ten problem; do niedawna kompilator zawsze osadzał nowy identyfikator GUID w wygenerowanym zestawie, aby zagwarantować, że dwie kompilacje nie będą binarnie identyczne, aby można było je rozróżnić!
Eric Lippert,
3
@EricLippert, jeśli dwie kompilacje różnią się tylko generowanym czasem (tj. Identyczną bazą kodu), czy nie powinniśmy traktować ich tak samo? Dzięki nowoczesnemu potokowi kompilacji CI / CD (Jenkins, TeamCity, CircleCI) będziemy mieć sposób na rozróżnienie kompilacji, ale z perspektywy aplikacji wdrażanie nowszych plików binarnych z identyczną bazą kodu nie wydaje się przydatne.
Dio Phung,
2
@DioPhung Jest na odwrót. Nie chcesz, aby dwie różne kompilacje miały ten sam identyfikator GUID, ponieważ w ten sposób system może zdecydować, której użyć. Dlatego najłatwiej jest wygenerować nowy identyfikator GUID za każdym razem; a następnie pojawia się efekt uboczny, który Eric opisuje jako niezamierzoną konsekwencję.
Graham
3
@vikingsteve Jak powiedziałem, jeszcze mniej przydatne byłoby zgłoszenie dwóch różnych wersji z tym samym identyfikatorem GUID, który byłby następnie zgłoszony do systemu jako to samo oprogramowanie. Spowodowałoby to całkowitą awarię dowolnego schematu obsługi administracyjnej, dlatego niezwykle ważne jest, aby identyfikatory GUID nigdy nie były duplikowane (z uzasadnionym prawdopodobieństwem!). Posiadanie różnych identyfikatorów GUID dla dwóch osobnych wersji tego samego kodu źródłowego jest co najwyżej trywialne. Więc w obliczu krytycznego scenariusza niepowodzenia, to, co uważasz za nieco nieprzydatne, tak naprawdę nie ma znaczenia.
Graham
4
@vikingsteve Część kodu pliku binarnego jest nadal taka sama (jeśli rozumiem, nie jestem programistą C #), to tylko niektóre metadane dołączone do pliku binarnego.
Captain Man,