¿Por qué una clase Java compila de manera diferente con una línea en blanco?

207

Tengo la siguiente clase de Java

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

Cuando compilo este archivo y ejecuto un sha256 en el archivo de clase resultante, obtengo

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Luego modifiqué la clase y agregué una línea en blanco como esta:

public class HelloWorld {

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

Nuevamente ejecuté un sha256 en la salida esperando obtener el mismo resultado, pero en su lugar obtuve

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

He leído en este artículo de TutorialsPoint que:

Una línea que contiene solo espacios en blanco, posiblemente con un comentario, se conoce como una línea en blanco, y Java la ignora por completo.

Entonces mi pregunta es, dado que Java ignora las líneas en blanco, ¿por qué el código de bytes compilado es diferente para ambos programas?

Es decir, la diferencia en que en HelloWorld.classun 0x03byte se reemplaza por un 0x04byte.

KNejad
fuente
45
Tenga en cuenta que el compilador no está obligado a ser determinista en la producción de archivos de clase, aunque normalmente lo son. Ver esta pregunta . Los archivos Jar por defecto no son reproducibles, es decir, incluso compilar el mismo código dará como resultado dos JAR diferentes. Esto se debe a que el orden de los archivos y las marcas de tiempo no coincidirán. Las construcciones reproducibles son posibles con una configuración específica.
Giacomo Alzetta
22
TutorialsPoint afirma que "Java ignora totalmente" las líneas en blanco. La Sección 3.4 de la Especificación del lenguaje Java dice lo contrario. ¿Cuál creer? ...
skomisa
37
@skomisa La especificación.
wizzwizz4
44
@GiacomoAlzetta ni siquiera hay un formulario de bytecode especificado para un solo archivo de bytecode. Por ejemplo, el orden de los miembros no está especificado, por lo que si el compilador usa los nuevos Sets inmutables con aleatorización internamente, podría producir un orden diferente en cada ejecución. También podría agregar un atributo personalizado que contenga el tiempo de compilación. Y así sucesivamente ...
Holger
15
@DioPhung otra lección aprendida: tutorialspoint no es una fuente confiable para buenos tutoriales
comenzando el

Respuestas:

331

Básicamente, los números de línea se guardan para la depuración, por lo que si cambia su código fuente de la manera en que lo hizo, su método comienza en una línea diferente y la clase compilada refleja la diferencia.

Federico klez Culloca
fuente
11
Eso también explica por qué difiere en los Bytes informados por el OP: end-of-transmissionsignifica el código ASCII 4 y end-of-textrepresenta el código ASCII 3
Ferrybig
160
Para probar esto experimentalmente, comparé los hash de los archivos de clase de la fuente de OP usando el -g:noneindicador al compilar (que elimina toda la información de depuración, ver aquí ) y obtuve el mismo hash en ambos escenarios.
Capitán Man
14
En apoyo formal de su respuesta, de la sección 3.4 ( "Terminadores de línea" ) de la Especificación del lenguaje Java para Java SE 11 : "A continuación, un compilador de Java divide la secuencia de caracteres de entrada Unicode en líneas al reconocer los terminadores de línea ... Las líneas definidas por terminadores de línea pueden determinar los números de línea producidos por un compilador de Java " .
skomisa
44
Un uso importante de estos números de línea es si se produce una excepción; Puede indicarle el número de línea de la excepción en el seguimiento de la pila.
gparyani
114

Puede ver el cambio mediante el uso javap -vque generará información detallada. Como otros ya mencionados, la diferencia estará en los números de línea:

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

Más precisamente, el archivo de clase difiere en la LineNumberTablesección:

El atributo LineNumberTable es un atributo opcional de longitud variable en la tabla de atributos de un atributo de Código (§4.7.3). Los depuradores pueden usarlo para determinar qué parte de la matriz de código corresponde a un número de línea dado en el archivo fuente original.

Si hay varios atributos LineNumberTable en la tabla de atributos de un atributo Code, entonces pueden aparecer en cualquier orden.

Puede haber más de un atributo LineNumberTable por línea de un archivo fuente en la tabla de atributos de un atributo Code. Es decir, los atributos LineNumberTable juntos pueden representar una línea dada de un archivo fuente, y no necesitan ser uno a uno con las líneas fuente.

Karol Dowbecki
fuente
57

La suposición de que "Java ignora las líneas en blanco" es incorrecta. Aquí hay un fragmento de código que se comporta de manera diferente dependiendo del número de líneas vacías antes del método 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]);
  }
}

Si no hay líneas vacías antes main, imprime "foo", pero con una línea vacía antes main, imprime"bar" .

Dado que el comportamiento en tiempo de ejecución es diferente, los .classarchivos deben ser diferentes, independientemente de las marcas de tiempo u otros metadatos.

Esto es válido para todos los idiomas que tienen acceso a los marcos de la pila con números de línea, no solo para Java.

Nota: si se compila con -g:none(sin ninguna información de depuración), los números de línea no se incluirán, getLineNumber()siempre regresa -1y el programa siempre imprime "bar", independientemente de la cantidad de saltos de línea.

Andrey Tyukin
fuente
11
También se puede imprimir Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk
1
@xehpuk La única forma en que pude obtener -1fue usando la -g:nonebandera. ¿Hay alguna otra forma de obtener esta excepción usando ordinario javac?
Andrey Tyukin
3
Supongo que solo con la -gopción. También hay -g:varsy -g:sourceque impide la generación de la LineNumberTable.
xehpuk
14

Además de los detalles de cualquier número de línea para la depuración, su manifiesto también puede almacenar la fecha y hora de compilación. Naturalmente, esto será diferente cada vez que compile.

Graham
fuente
14
C # también tiene este problema; hasta hace poco, el compilador siempre incrustaba un GUID nuevo en el ensamblaje generado para garantizar que dos compilaciones no fueran idénticas en binario, ¡para poder distinguirlas!
Eric Lippert
3
@EricLippert si dos compilaciones solo son diferentes por su tiempo generado (es decir, una base de código idéntica), ¿no deberíamos tratarlas como iguales? Con la canalización de compilación moderna de CI / CD (Jenkins, TeamCity, CircleCI), tendremos una manera de diferenciar entre compilaciones, pero desde la perspectiva de la aplicación, implementar binarios más nuevos con una base de código idéntica no parece ser útil.
Dio Phung
2
@DioPhung Es al revés. No desea que dos compilaciones diferentes tengan el mismo GUID, porque así es como el sistema puede decidir cuál usar. Por lo tanto, es más fácil generar un nuevo GUID cada vez; y luego obtienes el efecto secundario que Eric describe como una consecuencia no deseada.
Graham
3
@vikingsteve Como dije, sería aún menos útil informar dos compilaciones diferentes con el mismo GUID, que luego se informaría al sistema como el mismo software. Esto causaría una falla total de cualquier tipo de esquema de aprovisionamiento, por lo que es fundamental que los GUID nunca se dupliquen (¡con una probabilidad razonable!). Tener diferentes GUID para dos compilaciones separadas del mismo código fuente es una molestia trivial a lo sumo. Entonces, frente a un escenario de falla de misión crítica, lo que crees que es un poco inútil realmente no figura.
Graham
44
@vikingsteve La parte del código del binario sigue siendo la misma (si entiendo, no soy un desarrollador de C #), solo se adjuntan algunos metadatos al binario.
Capitán Man