Warum kompiliert eine Java-Klasse anders mit einer Leerzeile?

Lesezeit: 5 Minuten

Benutzer-Avatar
KNejad

Ich habe die folgende Java-Klasse

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

Wenn ich diese Datei kompiliere und einen sha256 auf der resultierenden Klassendatei ausführe, erhalte ich

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Als nächstes habe ich die Klasse geändert und eine leere Zeile wie diese hinzugefügt:

public class HelloWorld {

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

Wieder habe ich einen sha256 auf der Ausgabe ausgeführt, in der Erwartung, das gleiche Ergebnis zu erhalten, aber stattdessen habe ich es bekommen

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

Ich habe weitergelesen diesen TutorialsPoint-Artikel das:

Eine Zeile, die nur Leerzeichen enthält, möglicherweise mit einem Kommentar, wird als Leerzeile bezeichnet und von Java vollständig ignoriert.

Meine Frage ist also, da Java Leerzeilen ignoriert, warum ist der kompilierte Bytecode für beide Programme unterschiedlich?

Nämlich der Unterschied in dem in HelloWorld.class a 0x03 Byte wird durch a ersetzt 0x04 Byte.

  • Beachten Sie, dass der Compiler beim Erstellen von Klassendateien nicht verpflichtet ist, deterministisch zu sein, obwohl dies normalerweise der Fall ist. Siehe diese Frage. Jar-Dateien sind standardmäßig nicht reproduzierbar, dh sogar kompilieren das Gleiche Code führt zu zwei verschiedenen JARs. Das liegt daran, dass die Reihenfolge der Dateien und die Zeitstempel nicht übereinstimmen. Reproduzierbare Builds sind mit spezifischer Konfiguration möglich.

    – Giacomo Alzetta

    3. Oktober 2018 um 13:43 Uhr

  • TutorialsPoint behauptet das “Java ignoriert völlig” leere Zeilen. Abschnitt 3.4 der Java Language Specification sagt etwas anderes. Wem soll man glauben? …

    – Skomisa

    3. Oktober 2018 um 20:01 Uhr


  • @skomisa Die Spezifikation.

    – wizzwizz4

    3. Oktober 2018 um 20:09 Uhr

  • @GiacomoAlzetta Es gibt nicht einmal ein bestimmtes Bytecode-Formular für eine einzelne Bytecode-Datei. Beispielsweise ist die Reihenfolge der Mitglieder nicht festgelegt, wenn der Compiler also die neue unveränderliche verwendet SetBei interner Randomisierung könnte es bei jedem Lauf zu einer anderen Reihenfolge kommen. Es könnte auch ein benutzerdefiniertes Attribut hinzufügen, das die Kompilierzeit enthält. Usw…

    – Holger

    4. Oktober 2018 um 8:02 Uhr

  • @DioPhung noch eine Lektion gelernt: tutorialspoint ist keine verlässliche Quelle für gute Tutorials

    – jwenting

    4. Oktober 2018 um 8:34 Uhr

Grundsätzlich werden Zeilennummern zum Debuggen beibehalten. Wenn Sie also Ihren Quellcode so ändern, wie Sie es getan haben, beginnt Ihre Methode in einer anderen Zeile und die kompilierte Klasse spiegelt den Unterschied wider.

  • Das erklärt auch, warum es sich in den vom OP gemeldeten Bytes unterscheidet: end-of-transmission steht für den ASCII-Code 4 und end-of-text steht für den ASCII-Code 3

    – Ferrybig

    3. Oktober 2018 um 14:09 Uhr

  • Um dies experimentell zu beweisen, habe ich die Hashes der Klassendateien der OP-Quelle mit dem verglichen -g:none Flag beim Kompilieren (wodurch alle Debugging-Informationen entfernt werden, siehe hier) und erhielt in beiden Szenarien denselben Hash.

    – Kapitän Mann

    3. Oktober 2018 um 15:00 Uhr


  • Zur formellen Unterstützung Ihrer Antwort aus Abschnitt 3.4 (“Linienterminatoren”) des Java-Sprachspezifikation für Java SE 11: „Als nächstes teilt ein Java-Compiler die Folge von Unicode-Eingabezeichen in Zeilen, indem er Zeilenabschlusszeichen erkennt … Die Zeilen, die durch Zeilenabschlusszeichen definiert sind, können die Zeilennummern bestimmen, die von einem Java-Compiler erzeugt werden.

    – Skomisa

    3. Oktober 2018 um 19:53 Uhr

  • Eine wichtige Verwendung dieser Zeilennummern ist, wenn eine Ausnahme ausgelöst wird; Es kann Ihnen die Zeilennummer der Ausnahme im Stack-Trace mitteilen.

    – gparjani

    5. Oktober 2018 um 10:21 Uhr

Benutzer-Avatar
Karl Dowbecki

Sie können die Änderung mit sehen javap -v die ausführliche Informationen ausgibt. Wie bei anderen bereits erwähnten liegt der Unterschied in den Zeilennummern:

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

Genauer gesagt unterscheidet sich die Klassendatei in der LineNumberTable Sektion:

Das LineNumberTable-Attribut ist ein optionales Attribut mit variabler Länge in der Attributtabelle eines Code-Attributs (§4.7.3). Es kann von Debuggern verwendet werden, um zu bestimmen, welcher Teil des Code-Arrays einer bestimmten Zeilennummer in der ursprünglichen Quelldatei entspricht.

Wenn mehrere LineNumberTable-Attribute in der Attributtabelle eines Code-Attributs vorhanden sind, können sie in beliebiger Reihenfolge erscheinen.

Es kann mehr als ein LineNumberTable-Attribut pro Zeile einer Quelldatei in der Attributtabelle eines Code-Attributs geben. Das heißt, LineNumberTable-Attribute können zusammen eine gegebene Zeile einer Quelldatei darstellen und müssen nicht eins zu eins mit Quellzeilen sein.

Benutzer-Avatar
Andrej Tjukin

Die Annahme, dass “Java ignoriert Leerzeilen” ist falsch. Hier ist ein Codeausschnitt, der sich je nach Anzahl der leeren Zeilen vor der Methode unterschiedlich verhält 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]);
  }
}

Wenn davor keine Leerzeilen stehen maines druckt "foo"aber mit einer Leerzeile davor maines druckt "bar".

Da das Laufzeitverhalten unterschiedlich ist, wird die .class Dateien muss unterschiedlich sein, unabhängig von Zeitstempeln oder anderen Metadaten.

Dies gilt für jede Sprache, die Zugriff auf die Stapelrahmen mit Zeilennummern hat, nicht nur für Java.

Hinweis: Wenn es mit kompiliert ist -g:none (ohne Debugging-Informationen), dann werden die Zeilennummern nicht eingeschlossen, getLineNumber() kehrt immer zurück -1und das Programm druckt immer "bar"unabhängig von der Anzahl der Zeilenumbrüche.

  • Es kann auch drucken Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.

    – xehpuk

    5. Oktober 2018 um 9:22 Uhr

  • @xehpuk Der einzige Weg, wie ich eine bekommen könnte -1 war die zu verwenden -g:none Flagge. Gibt es eine andere Möglichkeit, diese Ausnahme mit normal zu erhalten javac?

    – Andrej Tjukin

    5. Oktober 2018 um 10:24 Uhr


  • Ich denke nur mit der -g Möglichkeit. Es gibt auch -g:vars und -g:source was die Erzeugung von verhindert LineNumberTable.

    – xehpuk

    5. Oktober 2018 um 11:12 Uhr

Neben Zeilennummerndetails zum Debuggen kann Ihr Manifest auch die Erstellungszeit und das Erstellungsdatum speichern. Dies wird natürlich bei jedem Kompilieren anders sein.

1354700cookie-checkWarum kompiliert eine Java-Klasse anders mit einer Leerzeile?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy