A criação de arquivos de class Java é determinista?

Ao usar o mesmo JDK (ou seja, o mesmo executável javac ), os arquivos de class gerados são sempre idênticos? Pode haver uma diferença dependendo do sistema operacional ou do hardware ? Exceto da versão do JDK, poderia haver algum outro fator que resultasse em diferenças? Existem opções de compilador para evitar diferenças? A diferença é apenas possível, em teoria, ou o javac do Oracle realmente produz arquivos de class diferentes para as mesmas opções de input e de compilador?

Atualização 1 Estou interessado na geração , ou seja, na saída do compilador, não se um arquivo de class pode ser executado em várias plataformas.

Atualização 2 Por ‘Same JDK’, eu também quero dizer o mesmo executável javac .

Atualização 3 Distinção entre diferença teórica e diferença prática nos compiladores da Oracle.

[EDITAR, adicionando pergunta parafraseada]
“Quais são as circunstâncias em que o mesmo executável javac, quando executado em uma plataforma diferente, produzirá bytecode diferente?”

Vamos colocar desta forma:

Eu posso facilmente produzir um compilador Java inteiramente em conformidade que nunca produza o mesmo arquivo .class duas vezes, dado o mesmo arquivo .java .

Eu poderia fazer isso ajustando todos os tipos de construção de bytecode ou simplesmente adicionando atributos supérfluos ao meu método (o que é permitido).

Dado que a especificação não requer que o compilador produza arquivos de class idênticos byte-by-byte, eu evitaria depender de tal resultado.

No entanto , as poucas vezes que eu verifiquei, compilando o mesmo arquivo de origem com o mesmo compilador com as mesmas opções (e as mesmas bibliotecas!) Resultaram nos mesmos arquivos .class .

Update: Eu recentemente deparei com este post interessante sobre a implementação do switch on String no Java 7 . Neste post, há algumas partes relevantes, que vou citar aqui (grifo meu):

Para tornar a saída do compilador previsível e repetível, os mapas e conjuntos usados ​​nessas estruturas de dados são LinkedHashMap e LinkedHashSet vez de apenas HashMaps e HashSets . Em termos de correção funcional do código gerado durante uma dada compilation, usar HashMap e HashSet seria ótimo ; a ordem de iteração não importa. No entanto, achamos benéfico que a saída do javac não varie com base nos detalhes de implementação das classs do sistema .

Isso ilustra claramente o problema: o compilador não precisa agir de maneira determinística, desde que corresponda à especificação. Os desenvolvedores de compiladores, no entanto, percebem que geralmente é uma boa ideia tentar (desde que não seja muito caro, provavelmente).

Não há obrigação para os compiladores de produzir o mesmo bytecode em cada plataforma. Você deve consultar o utilitário javac diferentes fornecedores para obter uma resposta específica.


Vou mostrar um exemplo prático para isso com a ordenação de arquivos.

Digamos que tenhamos dois arquivos jar: my1.jar e My2.jar . Eles são colocados no diretório lib , lado a lado. O compilador lê-os em ordem alfabética (já que isso é lib ), mas a ordem é my1.jar , My2.jar quando o sistema de arquivos não faz My2.jar maiúsculas e minúsculas e My2.jar , my1.jar se for sensível a maiúsculas e minúsculas.

O my1.jar tem uma class A.class com um método

 public class A { public static void a(String s) {} } 

O My2.jar tem o mesmo A.class , mas com assinatura de método diferente (aceita Object ):

 public class A { public static void a(Object o) {} } 

É claro que, se você tiver uma binding

 String s = "x"; Aa(s); 

ele irá compilar uma chamada de método com assinatura diferente em diferentes casos. Então, dependendo da sensibilidade do seu sistema de arquivos, você obterá uma class diferente como resultado.

Resposta curta – NÃO


Resposta longa

O bytecode não precisa ser o mesmo para plataformas diferentes. É o JRE (Java Runtime Environment) que sabe exatamente como executar o bytecode.

Se você passar pela especificação Java VM, saberá que isso não precisa ser verdade que o bytecode é o mesmo para diferentes plataformas.

Indo através do formato de arquivo de class , mostra a estrutura de um arquivo de class como

 ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } 

Verificar a versão menor e principal

minor_version, major_version

Os valores dos itens minor_version e major_version são os números das versões principal e secundária deste arquivo de class. Juntamente, um número de versão principal e um secundário determinam a versão do formato de arquivo de class. Se um arquivo de class tiver o número de versão principal M e um número de versão menor m, denotamos a versão de seu formato de arquivo de class como Mm. Assim, versões de formato de arquivo de class podem ser ordenadas lexicograficamente, por exemplo, 1,5 <2,0 <2,1. Uma implementação da máquina virtual Java pode suportar um formato de arquivo de classe da versão v se, e somente se, v estiver em algum intervalo contíguo Mi.0 v Mj.m. Somente a Sun pode especificar qual intervalo de versões uma implementação de máquina virtual Java em conformidade com um determinado nível de release da plataforma Java pode suportar.1

Ler mais através das notas de rodapé

1 A implementação da máquina virtual Java da versão 1.0.2 do JDK da Sun suporta as versões de formato de arquivo de class 45.0 a 45.3, inclusive. As versões JDK 1.1.X da Sun podem suportar formatos de arquivo de class de versões no intervalo de 45.0 a 45.65535, inclusive. As implementações da versão 1.2 da plataforma Java 2 podem suportar formatos de arquivo de class de versões no intervalo de 45.0 a 46.0 inclusive.

Então, investigar tudo isso mostra que os arquivos de class gerados em diferentes plataformas não precisam ser idênticos.

Em primeiro lugar, não há absolutamente nenhuma garantia na especificação. Um compilador em conformidade poderia carimbar o tempo de compilation no arquivo de class gerado como um atributo adicional (personalizado), e o arquivo de class ainda estaria correto. No entanto, ele produziria um arquivo diferente em nível de byte em cada compilation, e trivialmente.

Em segundo lugar, mesmo sem tais truques desagradáveis, não há razão para esperar que um compilador faça exatamente a mesma coisa duas vezes seguidas, a menos que sua configuração e sua input sejam idênticas nos dois casos. A especificação descreve o nome do arquivo de origem como um dos atributos padrão, e adicionar linhas em branco ao arquivo de origem pode alterar a tabela de números de linha.

Em terceiro lugar, nunca encontrei nenhuma diferença na compilation devido à plataforma host (além daquela que foi atribuída a diferenças no que estava no caminho de class). O código que variaria com base na plataforma (isto é, bibliotecas de código nativas) não faz parte do arquivo de class, e a geração real do código nativo do bytecode acontece depois que a class é carregada.

Em quarto lugar (e mais importante) cheira a um mau cheiro de processo (como um cheiro de código, mas de como você age no código) querer saber disso. Versão a fonte, se possível, não a compilation, e se você precisar versão a compilation, versão no nível de todo o componente e não em arquivos de class individuais. Para preferência, use um servidor de IC (como o Jenkins) para gerenciar o processo de transformar a origem em código executável.

Acredito que, se você usar o mesmo JDK, o código de byte gerado será sempre o mesmo, sem relação com o harware e o SO utilizados. A produção do código de bytes é feita pelo compilador java, que usa um algoritmo determinístico para “transformar” o código-fonte em código de bytes. Então, a saída será sempre a mesma. Nessas condições, apenas uma atualização no código-fonte afetará a saída.

No geral, devo dizer que não há garantia de que a mesma fonte produza o mesmo bytecode quando compilada pelo mesmo compilador, mas em uma plataforma diferente.

Eu examinaria cenários envolvendo diferentes idiomas (páginas de código), por exemplo, o Windows com suporte ao idioma japonês. Pense em caracteres multi-byte; a menos que o compilador sempre pressuponha que precisa suportar todos os idiomas que ele possa otimizar para o ASCII de 8 bits.

Há uma seção sobre compatibilidade binária na especificação da linguagem Java .

No âmbito da Compatibilidade Binária Release-to-Release no SOM (Forman, Conner, Danforth e Raper, Proceedings of OOPSLA ’95), os binários da linguagem de programação Java são binários compatíveis sob todas as transformações relevantes que os autores identificam (com algumas ressalvas em relação à adição de variables ​​de instância). Usando seu esquema, aqui está uma lista de algumas mudanças binárias compatíveis importantes que a linguagem de programação Java suporta:

• Reimplementando methods, construtores e inicializadores existentes para melhorar o desempenho.

• Alterar methods ou construtores para retornar valores em inputs para os quais eles anteriormente lançaram exceções que normalmente não devem ocorrer ou falhar entrando em um loop infinito ou causando um deadlock.

• Adicionar novos campos, methods ou construtores a uma class ou interface existente.

• Excluindo campos, methods ou construtores privados de uma class.

• Quando um pacote inteiro é atualizado, excluindo campos, methods ou construtores de classs e interfaces padrão (somente pacote) no pacote.

• Reordenar os campos, methods ou construtores em uma declaração de tipo existente.

• Movendo um método para cima na hierarquia de classs.

• Reordenar a lista de superinterfaces diretas de uma class ou interface.

• Inserindo nova class ou tipos de interface na hierarquia de tipos.

Este capítulo especifica padrões mínimos para compatibilidade binária garantidos por todas as implementações. A linguagem de programação Java garante a compatibilidade quando binários de classs e interfaces são misturados que não são conhecidos por serem de fonts compatíveis, mas cujas origens foram modificadas nas formas compatíveis descritas aqui. Observe que estamos discutindo a compatibilidade entre liberações de um aplicativo. Uma discussão sobre compatibilidade entre lançamentos da plataforma Java SE está além do escopo deste capítulo.

Java allows you write/compile code on one platform and run on different platform. AFAIK ; isso só será possível quando o arquivo de class gerado em uma plataforma diferente for igual ou tecnicamente idêntico.

Editar

O que quero dizer com o mesmo comentário técnico é isso. Eles não precisam ser exatamente iguais se você comparar byte por byte.

Assim, conforme a especificação, o arquivo .class de uma class em diferentes plataformas não precisa corresponder byte por byte.

Para a pergunta:

“Quais são as circunstâncias em que o mesmo executável javac, quando executado em uma plataforma diferente, produzirá bytecode diferente?”

O exemplo de compilation cruzada mostra como podemos usar a opção Javac: -versão de destino

Esse sinalizador gera arquivos de class que são compatíveis com a versão Java que especificamos ao chamar esse comando. Portanto, os arquivos de class serão diferentes dependendo dos atributos que fornecemos durante a compaliação usando essa opção.

Muito provavelmente, a resposta é “sim”, mas para ter uma resposta precisa, é necessário procurar algumas chaves ou geração de guid durante a compilation.

Não me lembro da situação em que isso ocorre. Por exemplo, para ter um ID para fins de serialização, ele é codificado, ou seja, gerado pelo programador ou pelo IDE.

PS Também JNI pode importar.

PPS descobri que o próprio javac é escrito em java. Isso significa que é idêntico em diferentes plataformas. Por isso, não geraria código diferente sem uma razão. Então, isso pode ser feito apenas com chamadas nativas.

Existem duas perguntas.

 Can there be a difference depending on the operating system or hardware? 

Esta é uma questão teórica, e a resposta é claramente, sim, pode haver. Como outros já disseram, a especificação não requer que o compilador produza arquivos de class idênticos byte-by-byte.

Mesmo que todo compilador atualmente existente produza o mesmo código de bytes em todas as circunstâncias (hardware diferente, etc.), a resposta amanhã pode ser diferente. Se você nunca planeja atualizar o javac ou seu sistema operacional, pode testar o comportamento dessa versão em suas circunstâncias específicas, mas os resultados podem ser diferentes se você for, por exemplo, do Java 7 Update 11 para o Java 7 Update 15.

 What are the circumstances where the same javac executable, when run on a different platform, will produce different bytecode? 

Isso é incognoscível.

Eu não sei se o gerenciamento de configuração é o seu motivo para fazer a pergunta, mas é um motivo compreensível para se importar. A comparação de códigos de bytes é um controle de TI legítimo, mas apenas para determinar se os arquivos de class foram alterados, e não para o topo para determinar se os arquivos de origem foram alterados.

Eu colocaria de outra forma.

Primeiro, acho que a questão não é sobre ser determinista:

É claro que é determinista: a aleatoriedade é difícil de conseguir na ciência da computação, e não há razão para que um compilador a introduza aqui por qualquer motivo.

Segundo, se você reformulá-lo por “quão similares são os arquivos de bytecode para um mesmo arquivo de código fonte?”, Então Não , você não pode confiar no fato de que eles serão semelhantes.

Uma boa maneira de ter certeza disso é deixar o .class (ou .pyc no meu caso) no seu estágio do git. Você perceberá que entre os diferentes computadores de sua equipe, o git notifica as alterações entre os arquivos .pyc, quando nenhuma alteração foi trazida para o arquivo .py (e o .pyc recompilado assim mesmo).

Pelo menos é o que eu observei. Então coloque * .pyc e * .class no seu .gitignore!