Perché una classe Java si compila in modo diverso con una riga vuota?


207

Ho la seguente classe Java

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

Quando compilo questo file ed eseguo uno sha256 sul file di classe risultante ottengo

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Successivamente ho modificato la classe e ho aggiunto una riga vuota come questa:

public class HelloWorld {

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

Ancora una volta ho eseguito uno sha256 sull'output aspettandomi di ottenere lo stesso risultato ma invece ho ottenuto

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

Ho letto su questo articolo TutorialsPoint che:

Una riga contenente solo spazi bianchi, possibilmente con un commento, è nota come riga vuota e Java lo ignora totalmente.

Quindi la mia domanda è, poiché Java ignora le righe vuote perché il bytecode compilato è diverso per entrambi i programmi?

Vale a dire la differenza in quella in HelloWorld.classun 0x03byte è sostituita da un 0x04byte.


45
Si noti che il compilatore non è obbligato ad essere deterministico nella produzione di file di classe, anche se normalmente lo sono. Vedere questa domanda . I file jar per impostazione predefinita non sono riproducibili, ovvero anche la compilazione dello stesso codice comporterà due JAR diversi. Questo perché l'ordine dei file e i timestamp non corrisponderanno. Build riproducibili sono possibili con una configurazione specifica.
Giacomo Alzetta,

22
TutorialsPoint afferma che "Java ignora totalmente" le righe vuote. La sezione 3.4 della specifica del linguaggio Java dice diversamente. A chi credere? ...
skomisa,

37
@skomisa Le specifiche.
wizzwizz4,

4
@GiacomoAlzetta non esiste nemmeno un modulo bytecode specificato per un singolo file bytecode. Ad esempio, l'ordine dei membri non è specificato, quindi se il compilatore utilizza i nuovi messaggi immutabili Setcon randomizzazione internamente, potrebbe produrre un ordine diverso ad ogni esecuzione. Potrebbe anche aggiungere un attributo personalizzato contenente il tempo di compilazione. E così via ...
Holger,

15
@DioPhung un'altra lezione appresa: tutorialspoint non è una fonte affidabile per buoni tutorial
jwenting

Risposte:


331

Fondamentalmente, i numeri di riga vengono conservati per il debug, quindi se si modifica il codice sorgente nel modo in cui è stato eseguito, il metodo inizia da una riga diversa e la classe compilata riflette la differenza.


11
Ciò spiega anche perché differisce nei byte riportati dall'OP: end-of-transmissionsta per il codice ASCII 4 e end-of-textsta per il codice ASCII 3
Ferrybig,

160
Per dimostrarlo sperimentalmente, ho confrontato gli hash dei file di classe del sorgente di OP usando il -g:noneflag durante la compilazione (che rimuove tutte le informazioni di debug, vedi qui ) e ho ottenuto lo stesso hash in entrambi gli scenari.
Captain Man,

14
A supporto formale della tua risposta, dalla sezione 3.4 ( "Terminatori di linea" ) della specifica del linguaggio Java per Java SE 11 : "Un compilatore Java successivamente divide la sequenza di caratteri di input Unicode in linee riconoscendo i terminatori di linea ... Le linee definite i terminatori per riga possono determinare i numeri di riga prodotti da un compilatore Java " .
skomisa,

4
Un uso importante di questi numeri di riga è se viene generata un'eccezione; può indicare il numero di riga dell'eccezione nella traccia dello stack.
gparyani,

114

Puoi vedere la modifica usando javap -vquale produrrà informazioni dettagliate. Come altri già menzionati, la differenza sarà nei numeri di riga:

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

Più precisamente il file di classe differisce nella LineNumberTablesezione:

L'attributo LineNumberTable è un attributo opzionale di lunghezza variabile nella tabella degli attributi di un attributo Code (§4.7.3). Può essere utilizzato dai debugger per determinare quale parte dell'array di codice corrisponde a un determinato numero di riga nel file di origine originale.

Se nella tabella degli attributi di un attributo Codice sono presenti più attributi LineNumberTable, possono apparire in qualsiasi ordine.

Possono esserci più di un attributo LineNumberTable per riga di un file di origine nella tabella degli attributi di un attributo Code. Ossia, gli attributi LineNumberTable possono insieme rappresentare una determinata riga di un file di origine e non devono necessariamente essere uno a uno con le righe di origine.


57

L'ipotesi che "Java ignori le righe vuote" è errata. Ecco uno snippet di codice che si comporta in modo diverso a seconda del numero di righe vuote prima del metodo 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]);
  }
}

Se non ci sono righe vuote prima main, stampa "foo", ma con una riga vuota prima main, stampa "bar".

Poiché il comportamento di runtime è diverso, i .classfile devono essere diversi, indipendentemente da eventuali timestamp o altri metadati.

Questo vale per ogni lingua che ha accesso ai frame dello stack con numeri di riga, non solo per Java.

Nota: se viene compilato -g:none(senza informazioni di debug), i numeri di riga non verranno inclusi, verranno getLineNumber()sempre restituiti -1e il programma verrà sempre stampato "bar", indipendentemente dal numero di interruzioni di riga.


11
Può anche stampare Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk,

1
@xehpuk L'unico modo in cui ho potuto ottenere -1era usare la -g:nonebandiera. C'è un altro modo per ottenere questa eccezione usando l'ordinario javac?
Andrey Tyukin,

3
Immagino solo con l' -gopzione. C'è anche -g:varse -g:sourceciò impedisce la generazione del LineNumberTable.
xehpuk,

14

Oltre ai dettagli del numero di riga per il debug, manifest può anche memorizzare la data e l'ora di creazione. Questo sarà naturalmente diverso ogni volta che compili.


14
Anche C # ha questo problema; fino a poco tempo fa il compilatore includeva sempre un nuovo GUID nell'assembly generato in modo da garantire che due build non fossero binarie identiche, in modo da poterle distinguere!
Eric Lippert,

3
@EricLippert se due build sono diverse solo per il loro tempo generato (cioè base di codice identica), non dovremmo trattarle allo stesso modo? Con la moderna pipeline di build CI / CD (Jenkins, TeamCity, CircleCI), avremo un modo per distinguere tra build, ma dal punto di vista dell'applicazione, la distribuzione di binari più recenti con base di codice identica non sembra essere utile.
Dio Phung,

2
@DioPhung È il contrario. Non vuoi che due build diverse abbiano lo stesso GUID, perché è così che il sistema può decidere quale utilizzare. Quindi è più semplice generare un nuovo GUID ogni volta; e poi ottieni l'effetto collaterale che Eric descrive come una conseguenza non intenzionale.
Graham,

3
@vikingsteve Come ho detto, sarebbe ancora meno utile che due build diverse venissero segnalate con lo stesso GUID, che sarebbe poi segnalato al sistema come lo stesso software. Ciò causerebbe un fallimento totale di qualsiasi tipo di schema di provisioning, quindi è fondamentale che i GUID non vengano mai duplicati (con ragionevole probabilità!). Avere GUID diversi per due build separate dello stesso codice sorgente è al massimo un banale fastidio. Quindi, di fronte a uno scenario di fallimento mission-critical, ciò che pensi sia leggermente inutile in realtà non appare.
Graham,

4
@vikingsteve La parte di codice del binario è sempre la stessa (se ho capito, non sono un sviluppatore C #), sono solo alcuni metadati che sono collegati al binario.
Captain Man,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.