Por que uma classe Java compila de maneira diferente com uma linha em branco?

207

Eu tenho a seguinte classe Java

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

Ao compilar esse arquivo e executar um sha256 no arquivo de classe resultante, recebo

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Em seguida, modifiquei a classe e adicionei uma linha em branco como esta:

public class HelloWorld {

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

Novamente, corri um sha256 na saída esperando obter o mesmo resultado, mas, em vez disso, obtive

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

Eu li neste artigo do TutorialsPoint que:

Uma linha contendo apenas espaço em branco, possivelmente com um comentário, é conhecida como linha em branco e o Java a ignora totalmente.

Portanto, minha pergunta é: como o Java ignora linhas em branco, por que o código de código compilado é diferente para os dois programas?

Ou seja, a diferença de que em HelloWorld.classum 0x03byte é substituída por um 0x04byte.

KNejad
fonte
45
Observe que o compilador não é obrigado a ser determinístico na produção de arquivos de classe, mesmo que normalmente sejam. Veja esta pergunta . Os arquivos jar, por padrão, não são reproduzíveis, ou seja, mesmo a compilação do mesmo código resultará em dois JARs diferentes. Isso ocorre porque a ordem dos arquivos e os carimbos de data e hora não coincidem. Compilações reproduzíveis são possíveis com configuração específica.
Giacomo Alzetta
22
O TutorialsPoint afirma que "Java ignora totalmente" as linhas em branco. A seção 3.4 da especificação da linguagem Java diz o contrário. Qual a crer ...?
skomisa
37
@skomisa A especificação.
Wizzwizz4
4
@GiacomoAlzetta não existe nem um formulário de bytecode especificado para um único arquivo de bytecode. Por exemplo, a ordem dos membros não é especificada; portanto, se o compilador usar os novos Sets imutáveis com randomização internamente, ele poderá produzir uma ordem diferente em cada execução. Também poderia adicionar um atributo personalizado contendo o tempo de compilação. E assim por diante ...
Holger
15
@DioPhung outra lição aprendida: tutorialspoint não é uma fonte confiável para bons tutoriais
jwenting

Respostas:

331

Basicamente, os números de linha são mantidos para depuração; portanto, se você alterar seu código-fonte da maneira que fez, seu método inicia em uma linha diferente e a classe compilada reflete a diferença.

Federico klez Culloca
fonte
11
Isso também explica por que é diferente nos bytes relatados pelo OP: end-of-transmissionsignifica o código ASCII 4 e end-of-texto código ASCII 3
Ferrybig 3/18/18
160
Para provar isso experimentalmente, comparei os hashes dos arquivos de classe da fonte do OP usando o -g:nonesinalizador ao compilar (que remove todas as informações de depuração, veja aqui ) e obtive o mesmo hash nos dois cenários.
Captain Man
14
Em suporte formal à sua resposta, da seção 3.4 ( "Terminadores de Linha" ) da Especificação de Linguagem Java para Java SE 11 : "Um compilador Java a seguir divide a sequência de caracteres de entrada Unicode em linhas, reconhecendo os terminadores de linha ... As linhas definidas por terminadores de linha podem determinar os números de linha produzidos por um compilador Java " .
Skomisa #
4
Um uso importante desses números de linha é se uma exceção é lançada; pode informar o número da linha da exceção no rastreamento de pilha.
Gparyani #
114

Você pode ver a alteração usando o javap -vque produzirá informações detalhadas. Como outros já mencionados, a diferença estará nos números de linha:

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

Mais precisamente, o arquivo de classe difere na LineNumberTableseção:

O atributo LineNumberTable é um atributo opcional de comprimento variável na tabela de atributos de um atributo Code (§4.7.3). Pode ser usado pelos depuradores para determinar qual parte da matriz de códigos corresponde a um número de linha especificado no arquivo de origem original.

Se vários atributos LineNumberTable estiverem presentes na tabela de atributos de um atributo Code, eles poderão aparecer em qualquer ordem.

Pode haver mais de um atributo LineNumberTable por linha de um arquivo de origem na tabela de atributos de um atributo Code. Ou seja, os atributos LineNumberTable podem juntos representar uma determinada linha de um arquivo de origem e não precisam ser individuais com as linhas de origem.

Karol Dowbecki
fonte
57

A suposição de que "Java ignora linhas em branco" está errada. Aqui está um trecho de código que se comporta de maneira diferente, dependendo do número de linhas vazias antes do 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]);
  }
}

Se não houver linhas vazias antes main, ela será impressa "foo", mas com uma linha vazia antes main, ela será impressa "bar".

Como o comportamento do tempo de execução é diferente, os .classarquivos devem ser diferentes, independentemente de qualquer registro de data e hora ou outros metadados.

Isso vale para todos os idiomas que têm acesso aos quadros da pilha com números de linha, não apenas para Java.

Nota: se for compilado com -g:none(sem nenhuma informação de depuração), os números de linha não serão incluídos, getLineNumber()sempre retornarão -1e o programa sempre será impresso "bar", independentemente do número de quebras de linha.

Andrey Tyukin
fonte
11
Também pode imprimir Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk
1
@xehpuk A única maneira de conseguir um -1era usando a -g:nonebandeira. Existe alguma outra maneira de obter essa exceção usando o ordinário javac?
Andrey Tyukin
3
Eu acho que apenas com a -gopção. Há também -g:varse -g:sourceque impede a geração do LineNumberTable.
xehpuk
14

Além de qualquer número de linha para depuração, seu manifesto também pode armazenar a data e a hora da compilação. Naturalmente, isso será diferente toda vez que você compilar.

Graham
fonte
14
C # também tem esse problema; até recentemente, o compilador sempre incorporava um GUID novo no assembly gerado, para garantir que duas compilações não seriam binárias idênticas, para que você pudesse diferenciá-las!
Eric Lippert
3
@ Ericricippert, se duas compilações são diferentes apenas pelo tempo gerado (ou seja, base de código idêntica), não devemos tratá-las da mesma forma? Com o pipeline de compilação de CI / CD moderno (Jenkins, TeamCity, CircleCI), teremos uma maneira de diferenciar entre compilações, mas da perspectiva do aplicativo, implantar binários mais novos com base de código idêntica não parece ser útil.
Dio Phung
2
@DioPhung É o contrário. Você não deseja que duas construções diferentes tenham o mesmo GUID, porque é assim que o sistema pode decidir qual usar. Portanto, é mais fácil gerar um novo GUID a cada vez; e então você obtém o efeito colateral que Eric descreve como uma conseqüência não intencional.
Graham
3
@vikingsteve Como eu disse, seria ainda menos útil relatar duas compilações diferentes com o mesmo GUID, que seriam relatadas ao sistema como sendo o mesmo software. Isso causaria falha total de qualquer tipo de esquema de provisionamento; portanto, é essencial que os GUIDs nunca sejam duplicados (com probabilidade razoável!). Ter GUIDs diferentes para duas compilações separadas do mesmo código-fonte é um aborrecimento trivial, no máximo. Portanto, diante de um cenário de falha de missão crítica, o que você acha que é um pouco inútil realmente não aparece.
Graham
4
@vikingsteve A parte do código do binário ainda é a mesma (se eu entendo, não sou um dev C #), são apenas alguns metadados anexados ao binário.
Capitão Man