Pourquoi une classe Java se compile-t-elle différemment avec une ligne vierge?

207

J'ai la classe Java suivante

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

Lorsque je compile ce fichier et exécute un sha256 sur le fichier de classe résultant, j'obtiens

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Ensuite, j'ai modifié la classe et ajouté une ligne vierge comme celle-ci:

public class HelloWorld {

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

Encore une fois, j'ai exécuté un sha256 sur la sortie en espérant obtenir le même résultat, mais à la place, j'ai

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

J'ai lu sur cet article TutorialsPoint que:

Une ligne contenant uniquement des espaces blancs, éventuellement avec un commentaire, est connue comme une ligne vierge et Java l'ignore totalement.

Donc ma question est, puisque Java ignore les lignes vides, pourquoi le bytecode compilé est-il différent pour les deux programmes?

A savoir la différence en ce que dans HelloWorld.classun 0x03octet est remplacée par un 0x04octet.

KNejad
la source
45
Notez que le compilateur n'est pas obligé d'être déterministe dans la production de fichiers de classe, même s'ils le sont normalement. Voir cette question . Les fichiers Jar par défaut ne sont pas reproductibles, c'est-à-dire que même la compilation du même code entraînera deux JAR différents. En effet, l'ordre des fichiers et les horodatages ne correspondent pas. Des builds reproductibles sont possibles avec une configuration spécifique.
Giacomo Alzetta
22
TutorialsPoint prétend que "Java ignore totalement" les lignes vides. La section 3.4 de la spécification du langage Java indique le contraire. Lequel croire? ...
skomisa
37
@skomisa La spécification.
wizzwizz4
4
@GiacomoAlzetta, il n'y a même pas de formulaire de bytecode spécifié pour un seul fichier de bytecode. Par exemple, l'ordre des membres n'est pas spécifié, donc si le compilateur utilise les nouveaux Sets immuables avec randomisation en interne, il pourrait produire un ordre différent à chaque exécution. Il pourrait également ajouter un attribut personnalisé contenant le temps de compilation. Et ainsi de suite…
Holger
15
@DioPhung une autre leçon apprise: tutorialspoint n'est pas une source fiable pour de bons tutoriels
jwenting

Réponses:

331

Fondamentalement, les numéros de ligne sont conservés pour le débogage, donc si vous modifiez votre code source comme vous l'avez fait, votre méthode démarre sur une ligne différente et la classe compilée reflète la différence.

Federico klez Culloca
la source
11
Cela explique également pourquoi son diffère dans les octets rapportés par l'OP: end-of-transmissionreprésente le code ASCII 4 et end-of-textreprésente le code ASCII 3
Ferrybig
160
Pour le prouver expérimentalement, j'ai comparé les hachages des fichiers de classe de la source OP en utilisant l' -g:noneindicateur lors de la compilation (ce qui supprime toutes les informations de débogage, voir ici ) et j'ai obtenu le même hachage dans les deux scénarios.
Captain Man
14
À l'appui formel de votre réponse, de la section 3.4 ( «Terminateurs de ligne» ) de la spécification de langage Java pour Java SE 11 : «Un compilateur Java divise ensuite la séquence de caractères d'entrée Unicode en lignes en reconnaissant les terminateurs de ligne ... Les lignes définies par les terminateurs de ligne peuvent déterminer les numéros de ligne produits par un compilateur Java " .
skomisa
4
Une utilisation importante de ces numéros de ligne est si une exception est levée; il peut vous indiquer le numéro de ligne de l'exception dans la trace de pile.
gparyani
114

Vous pouvez voir le changement en utilisant javap -vqui produira des informations détaillées. Comme d'autres déjà mentionnés, la différence sera dans les numéros de ligne:

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

Plus précisément, le fichier de classe diffère dans la LineNumberTablesection:

L'attribut LineNumberTable est un attribut facultatif de longueur variable dans la table des attributs d'un attribut Code (§4.7.3). Il peut être utilisé par les débogueurs pour déterminer quelle partie du tableau de code correspond à un numéro de ligne donné dans le fichier source d'origine.

Si plusieurs attributs LineNumberTable sont présents dans la table des attributs d'un attribut Code, ils peuvent apparaître dans n'importe quel ordre.

Il peut y avoir plus d'un attribut LineNumberTable par ligne d'un fichier source dans la table des attributs d'un attribut Code. Autrement dit, les attributs LineNumberTable peuvent représenter ensemble une ligne donnée d'un fichier source et n'ont pas besoin d'être un à un avec des lignes source.

Karol Dowbecki
la source
57

L'hypothèse que "Java ignore les lignes vierges" est fausse. Voici un extrait de code qui se comporte différemment selon le nombre de lignes vides avant la méthode 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]);
  }
}

S'il n'y a pas de lignes vides avant main, il s'imprime "foo", mais avec une ligne vide avant main, il s'imprime "bar".

Étant donné que le comportement d'exécution est différent, les .classfichiers doivent être différents, quels que soient les horodatages ou autres métadonnées.

Cela vaut pour chaque langue qui a accès aux cadres de pile avec des numéros de ligne, pas seulement pour Java.

Remarque: s'il est compilé avec -g:none(sans aucune information de débogage), les numéros de ligne ne seront pas inclus, seront getLineNumber()toujours renvoyés -1et le programme s'imprimera toujours "bar", quel que soit le nombre de sauts de ligne.

Andrey Tyukin
la source
11
Il peut également imprimer Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk
1
@xehpuk La seule façon d'obtenir un -1était d'utiliser le -g:nonedrapeau. Existe-t-il un autre moyen d'obtenir cette exception en utilisant l'ordinaire javac?
Andrey Tyukin
3
Je suppose qu'avec l' -goption. Il y a aussi -g:varset -g:sourcequi empêche la génération du LineNumberTable.
xehpuk
14

En plus des détails du numéro de ligne pour le débogage, votre manifeste peut également stocker l'heure et la date de génération. Ce sera naturellement différent à chaque compilation.

Graham
la source
14
C # a également ce problème; jusqu'à récemment, le compilateur incorporait toujours un nouveau GUID dans l'assembly généré afin que vous soyez assuré que deux builds ne seraient pas identiques en binaire, afin que vous puissiez les distinguer!
Eric Lippert
3
@EricLippert si deux builds ne sont différents que par leur temps généré (c'est-à-dire une base de code identique), ne devrions-nous pas les traiter comme les mêmes? Avec le pipeline de génération de CI / CD moderne (Jenkins, TeamCity, CircleCI), nous aurons un moyen de différencier les générations, mais du point de vue des applications, le déploiement de nouveaux binaires avec une base de code identique ne semble pas être utile.
Dio Phung
2
@DioPhung C'est l'inverse. Vous ne voulez pas que deux versions différentes aient le même GUID, car c'est ainsi que le système peut décider lequel utiliser. Il est donc plus facile de générer un nouveau GUID à chaque fois; et puis vous obtenez l'effet secondaire que Eric décrit comme une conséquence involontaire.
Graham
3
@vikingsteve Comme je l'ai dit, il serait encore moins utile que deux versions différentes soient signalées avec le même GUID, qui serait ensuite signalé au système comme étant le même logiciel. Cela entraînerait l'échec total de tout type de schéma d'approvisionnement, il est donc essentiel que les GUID ne soient jamais dupliqués (dans une probabilité raisonnable!). Avoir des GUID différents pour deux versions distinctes du même code source est tout au plus une gêne triviale. Donc, face à un scénario d'échec critique, ce que vous pensez être légèrement inutile ne figure vraiment pas.
Graham
4
@vikingsteve La partie code du binaire est toujours la même (si je comprends bien, je ne suis pas un développeur C #), c'est juste quelques métadonnées qui sont attachées au binaire.
Captain Man