Atualmente, existem (Java 6) coisas que você pode fazer no bytecode Java que não pode ser feito na linguagem Java?
Eu sei que ambos são Turing completos, então leia "pode fazer" como "pode fazer significativamente mais rápido / melhor ou apenas de uma maneira diferente".
Estou pensando em bytecodes extras como invokedynamic
, que não podem ser gerados usando Java, exceto que um específico é para uma versão futura.
rol
no assembler, que você não pode escrever em C ++.(x<<n)|(x>>(32-n))
com umarol
instrução.Respostas:
Tanto quanto sei, não há recursos importantes nos bytecodes suportados pelo Java 6 que também não sejam acessíveis a partir do código-fonte Java. A principal razão para isso é obviamente que o bytecode Java foi projetado com a linguagem Java em mente.
Existem alguns recursos que não são produzidos pelos compiladores Java modernos, no entanto:
A
ACC_SUPER
bandeira :Este é um sinalizador que pode ser definido em uma classe e especifica como um caso de canto específico do
invokespecial
bytecode é tratado para essa classe. Ele é definido por todos os compiladores Java modernos (onde "moderno" é> = Java 1.1, se bem me lembro) e apenas os compiladores Java antigos produziram arquivos de classe onde isso não estava definido. Este sinalizador existe apenas por motivos de compatibilidade com versões anteriores. Observe que a partir do Java 7u51, o ACC_SUPER é completamente ignorado devido a motivos de segurança.Os
jsr
/ret
bytecodes.Esses bytecodes foram usados para implementar sub-rotinas (principalmente para implementar
finally
blocos). Eles não são mais produzidos desde o Java 6 . A razão para a sua descontinuação é que eles complicam muito a verificação estática sem grandes ganhos (ou seja, o código que usa quase sempre pode ser reimplementado com saltos normais com muito pouca sobrecarga).Ter dois métodos em uma classe que diferem apenas no tipo de retorno.
A especificação da linguagem Java não permite dois métodos na mesma classe quando diferem apenas no tipo de retorno (ou seja, mesmo nome, mesma lista de argumentos, ...). A especificação da JVM, no entanto, não possui essa restrição; portanto, um arquivo de classe pode conter dois métodos, não há como produzir um arquivo de classe usando o compilador Java normal. Há um bom exemplo / explicação nesta resposta .
fonte
a
outraA
dentro do arquivo JAR. Levei cerca de meia hora de descompactar em uma máquina Windows antes de perceber onde estavam as aulas ausentes. :)'.'
,';'
,'['
, ou'/'
. Os nomes dos métodos são os mesmos, mas eles também não podem conter'<'
or'>'
. (Com as notáveis exceções de<init>
e<clinit>
para instância e construtores estáticos.) Gostaria de salientar que, se você está seguindo a especificação estritamente, os nomes de classe são realmente muito mais restrito, mas as restrições não são aplicadas."throws ex1, ex2, ..., exn"
como parte das assinaturas do método; você não pode adicionar cláusulas de exceção a métodos substituídos. MAS, a JVM não se importava. Portanto, apenas osfinal
métodos são verdadeiramente garantidos pela JVM como livres de exceção - além deRuntimeException
s eError
s, é claro. Tanto para o tratamento de exceção verificada: DDepois de trabalhar com o código de bytes Java por um bom tempo e fazer algumas pesquisas adicionais sobre este assunto, aqui está um resumo das minhas descobertas:
Executar código em um construtor antes de chamar um super construtor ou construtor auxiliar
Na linguagem de programação Java (JPL), a primeira instrução de um construtor deve ser uma invocação de um super construtor ou outro construtor da mesma classe. Isso não se aplica ao código de bytes Java (JBC). No código de bytes, é absolutamente legítimo executar qualquer código antes de um construtor, desde que:
Defina os campos da instância antes de chamar um super construtor ou construtor auxiliar
Como mencionado anteriormente, é perfeitamente legal definir um valor de campo de uma instância antes de chamar outro construtor. Existe até um hack herdado que permite explorar esse "recurso" nas versões Java anteriores ao 6:
Dessa forma, um campo pode ser definido antes da invocação do super construtor, o que não é mais possível. No JBC, esse comportamento ainda pode ser implementado.
Ramificar uma chamada de super construtor
Em Java, não é possível definir uma chamada de construtor como
Até o Java 7u23, o verificador da HotSpot VM, no entanto, não atendia a essa verificação, razão pela qual era possível. Isso foi usado por várias ferramentas de geração de código como uma espécie de hack, mas não é mais legal implementar uma classe como esta.Este último foi apenas um bug nesta versão do compilador. Nas versões mais recentes do compilador, isso é novamente possível.
Definir uma classe sem nenhum construtor
O compilador Java sempre implementará pelo menos um construtor para qualquer classe. No código de bytes Java, isso não é necessário. Isso permite a criação de classes que não podem ser construídas, mesmo quando se usa reflexão. No entanto, o uso
sun.misc.Unsafe
ainda permite a criação de tais instâncias.Definir métodos com assinatura idêntica, mas com tipo de retorno diferente
Na JPL, um método é identificado como exclusivo por seu nome e seus tipos de parâmetros brutos. No JBC, o tipo de retorno bruto é considerado adicionalmente.
Definir campos que não diferem por nome, mas apenas por tipo
Um arquivo de classe pode conter vários campos com o mesmo nome, desde que declarem um tipo de campo diferente. A JVM sempre se refere a um campo como uma tupla de nome e tipo.
Lance exceções verificadas não declaradas sem capturá-las
O tempo de execução Java e o código de byte Java não estão cientes do conceito de exceções verificadas. É apenas o compilador Java que verifica se as exceções verificadas são sempre capturadas ou declaradas se forem lançadas.
Usar invocação de método dinâmico fora das expressões lambda
A chamada chamada de método dinâmico pode ser usada para qualquer coisa, não apenas para expressões lambda do Java. O uso desse recurso permite, por exemplo, alternar a lógica de execução em tempo de execução. Muitas linguagens de programação dinâmica que se resumem ao JBC melhoraram seu desempenho usando esta instrução. No código de byte Java, você também pode emular expressões lambda no Java 7 em que o compilador ainda não permitiu o uso de invocação de método dinâmico enquanto a JVM já entendia a instrução.
Use identificadores que normalmente não são considerados legais
Já imaginou usar espaços e uma quebra de linha no nome do seu método? Crie seu próprio JBC e boa sorte para revisão de código. Os únicos caracteres ilegais para identificadores são
.
,;
,[
e/
. Além disso, métodos que não são nomeados<init>
ou<clinit>
não podem conter<
e>
.Reatribuir
final
parâmetros ou athis
referênciafinal
parâmetros não existem no JBC e, consequentemente, podem ser reatribuídos. Qualquer parâmetro, incluindo athis
referência, é armazenado apenas em uma matriz simples na JVM, o que permite reatribuir athis
referência no índice0
dentro de um único quadro de método.Reatribuir
final
camposDesde que um campo final seja atribuído dentro de um construtor, é legal reatribuir esse valor ou até mesmo não atribuir um valor. Portanto, os dois construtores a seguir são legais:
Para
static final
campos, é permitido até reatribuir os campos fora do inicializador de classe.Trate os construtores e o inicializador de classe como se fossem métodos
Esse é um recurso mais conceitual, mas os construtores não são tratados de maneira diferente no JBC que os métodos normais. É apenas o verificador da JVM que garante que os construtores chamam outro construtor legal. Fora isso, é apenas uma convenção de nomenclatura Java que os construtores devem ser chamados
<init>
e que o inicializador de classe é chamado<clinit>
. Além dessa diferença, a representação de métodos e construtores é idêntica. Como Holger apontou em um comentário, você pode até definir construtores com tipos de retorno diferentes devoid
ou um inicializador de classe com argumentos, mesmo que não seja possível chamar esses métodos.Crie registros assimétricos * .
Ao criar um registro
O javac irá gerar um arquivo de classe com um único campo nomeado
bar
, um método acessador nomeadobar()
e um construtor usando um únicoObject
. Além disso, um atributo de registro parabar
é adicionado. Ao gerar manualmente um registro, é possível criar, uma forma diferente de construtor, pular o campo e implementar o acessador de maneira diferente. Ao mesmo tempo, ainda é possível fazer a API de reflexão acreditar que a classe representa um registro real.Chame qualquer super método (até Java 1.1)
No entanto, isso só é possível para as versões 1 e 1.1 do Java. No JBC, os métodos são sempre despachados em um tipo de destino explícito. Isso significa que para
foi possível implementar
Qux#baz
para invocarFoo#baz
enquanto pulavaBar#baz
. Embora ainda seja possível definir uma chamada explícita para chamar outra implementação de super método que não seja a superclasse direta, isso não tem mais efeito nas versões Java após a 1.1. No Java 1.1, esse comportamento era controlado pela configuração doACC_SUPER
sinalizador que permitiria o mesmo comportamento que chama apenas a implementação direta da superclasse.Definir uma chamada não virtual de um método declarado na mesma classe
Em Java, não é possível definir uma classe
O código acima sempre resultará em um
RuntimeException
quandofoo
é chamado em uma instância deBar
. Não é possível definir oFoo::foo
método para chamar seu própriobar
método, definido emFoo
. Comobar
é um método de instância não particular, a chamada é sempre virtual. No entanto, com o código de bytes, é possível definir a invocação para usar oINVOKESPECIAL
código de operação que vincula diretamente abar
chamada do métodoFoo::foo
àFoo
versão. Esse opcode é normalmente usado para implementar invocações de super métodos, mas você pode reutilizá-lo para implementar o comportamento descrito.Anotações do tipo granulação fina
Em Java, as anotações são aplicadas de acordo com o
@Target
que as anotações declaram. Usando a manipulação de código de bytes, é possível definir anotações independentemente desse controle. Além disso, é possível, por exemplo, anotar um tipo de parâmetro sem anotar o parâmetro, mesmo que a@Target
anotação se aplique aos dois elementos.Defina qualquer atributo para um tipo ou seus membros
Na linguagem Java, só é possível definir anotações para campos, métodos ou classes. No JBC, você pode basicamente incorporar qualquer informação nas classes Java. Para usar essas informações, você não pode mais confiar no mecanismo de carregamento de classe Java, mas precisa extrair as meta informações por conta própria.
Overflow e implicitamente atribuir
byte
,short
,char
eboolean
valoresOs últimos tipos primitivos normalmente não são conhecidos no JBC, mas são definidos apenas para tipos de matriz ou para descritores de campo e método. Nas instruções do código de bytes, todos os tipos nomeados ocupam o espaço de 32 bits, o que permite representá-los como
int
. Oficialmente, apenas osint
,float
,long
edouble
existem tipos no código byte que toda a necessidade de conversão explícita pelo Estado de verificador do JVM.Não libera um monitor
Na
synchronized
verdade, um bloco é composto de duas instruções, uma para adquirir e outra para liberar um monitor. No JBC, você pode adquirir um sem liberá-lo.Nota : Em implementações recentes do HotSpot, isso leva a um
IllegalMonitorStateException
no final de um método ou a uma liberação implícita se o método for finalizado por uma exceção em si.Adicione mais de uma
return
instrução a um inicializador de tipoEm Java, mesmo um inicializador de tipo trivial como
é ilegal. No código de bytes, o inicializador de tipo é tratado como qualquer outro método, ou seja, as instruções de retorno podem ser definidas em qualquer lugar.
Criar loops irredutíveis
O compilador Java converte loops em instruções goto no código de bytes Java. Tais instruções podem ser usadas para criar loops irredutíveis, o que o compilador Java nunca cria.
Definir um bloco de captura recursivo
No código de bytes Java, você pode definir um bloco:
Uma instrução semelhante é criada implicitamente ao usar um
synchronized
bloco em Java, em que qualquer exceção ao liberar um monitor retorna à instrução para liberá-lo. Normalmente, nenhuma exceção deve ocorrer em tal instrução, mas se ocorrer (por exemplo, a obsoletaThreadDeath
), o monitor ainda será liberado.Chame qualquer método padrão
O compilador Java requer que várias condições sejam atendidas para permitir a chamada de um método padrão:
B
estender a interface,A
mas não substituir um métodoA
, o método ainda poderá ser chamado.Para código de bytes Java, apenas a segunda condição conta. O primeiro é, no entanto, irrelevante.
Invoque um super método em uma instância que não seja
this
O compilador Java apenas permite chamar um método super (ou padrão da interface) em instâncias de
this
. No código de bytes, no entanto, também é possível invocar o super método em uma instância do mesmo tipo semelhante à seguinte:Acessar membros sintéticos
No código de bytes Java, é possível acessar membros sintéticos diretamente. Por exemplo, considere como no exemplo a seguir a instância externa de outra
Bar
instância é acessada:Isso geralmente é verdade para qualquer campo, classe ou método sintético.
Definir informações de tipo genérico fora de sincronização
Embora o tempo de execução Java não processe tipos genéricos (depois que o compilador Java aplica o apagamento do tipo), essas informações ainda são anexadas a uma classe compilada como meta-informação e tornadas acessíveis por meio da API de reflexão.
O verificador não verifica a consistência desses
String
valores codificados por metadados . Portanto, é possível definir informações sobre tipos genéricos que não correspondem à eliminação. Como concepção, as seguintes afirmações podem ser verdadeiras:Além disso, a assinatura pode ser definida como inválida, de modo que uma exceção de tempo de execução seja lançada. Essa exceção é lançada quando as informações são acessadas pela primeira vez e avaliadas preguiçosamente. (Semelhante aos valores da anotação com erro.)
Anexar informações meta meta apenas para certos métodos
O compilador Java permite incorporar o nome do parâmetro e as informações do modificador ao compilar uma classe com o
parameter
sinalizador ativado. No formato de arquivo da classe Java, essas informações são armazenadas por método, o que torna possível incorporar apenas essas informações a determinados métodos.Estrague tudo e faça um crash pesado na sua JVM
Como exemplo, no código de byte Java, você pode definir para chamar qualquer método em qualquer tipo. Normalmente, o verificador reclama se um tipo não conhece esse método. No entanto, se você invocar um método desconhecido em uma matriz, encontrei um bug em alguma versão da JVM, na qual o verificador perderá isso e sua JVM terminará assim que a instrução for chamada. Isso dificilmente é um recurso, mas é tecnicamente algo que não é possível com o Java compilado por javac . Java tem algum tipo de validação dupla. A primeira validação é aplicada pelo compilador Java, a segunda pela JVM quando uma classe é carregada. Ignorando o compilador, você pode encontrar um ponto fraco na validação do verificador. Esta é mais uma afirmação geral do que um recurso.
Anotar o tipo de receptor de um construtor quando não houver classe externa
Desde o Java 8, métodos não estáticos e construtores de classes internas podem declarar um tipo de receptor e anotar esses tipos. Os construtores de classes de nível superior não podem anotar seu tipo de receptor, pois a maioria não declara um.
Como
Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
, no entanto, retorna umaAnnotatedType
representaçãoFoo
, é possível incluir anotações de tipo paraFoo
o construtor diretamente no arquivo de classe em que essas anotações são lidas posteriormente pela API de reflexão.Use instruções de código de bytes não utilizados / herdados
Como outros o nomearam, também o incluirei. O Java anteriormente fazia uso de sub-rotinas pelas instruções
JSR
eRET
. A JBC até conhecia seu próprio tipo de endereço de retorno para esse fim. No entanto, o uso de sub-rotinas complicou demais a análise de código estático, razão pela qual essas instruções não são mais usadas. Em vez disso, o compilador Java duplicará o código que compila. No entanto, isso basicamente cria uma lógica idêntica e é por isso que eu realmente não considero alcançar algo diferente. Da mesma forma, você pode, por exemplo, adicionar oNOOP
instrução de código de bytes que também não é usada pelo compilador Java, mas isso também não permitiria que você conseguisse algo novo. Como apontado no contexto, essas "instruções de recurso" mencionadas agora são removidas do conjunto de códigos legais que os tornam ainda menos um recurso.fonte
<clinit>
método definindo métodos com o nome,<clinit>
mas aceitando parâmetros ou tendo umvoid
tipo de não retorno. Mas esses métodos não são muito úteis, a JVM os ignorará e o código de bytes não poderá invocá-los. O único uso seria confundir os leitores.IllegalMonitorStateException
se você omitiu amonitorexit
instrução. E, no caso de uma saída excepcional do método que falhou na execução de amonitorexit
, ela redefine o monitor silenciosamente.Aqui estão alguns recursos que podem ser executados no bytecode Java, mas não no código-fonte Java:
Lançar uma exceção verificada de um método sem declarar que o método a lança. As exceções verificadas e não verificadas são verificadas apenas pelo compilador Java, não pela JVM. Por isso, por exemplo, o Scala pode lançar exceções verificadas dos métodos sem declará-las. Embora, com os genéricos Java, exista uma solução alternativa chamada sneaky throw .
Tendo dois métodos em uma classe que diferem apenas no tipo de retorno, como já mencionado na resposta de Joachim : A especificação da linguagem Java não permite dois métodos na mesma classe quando eles diferem apenas no tipo de retorno (ou seja, mesmo nome, mesma lista de argumentos, ...) A especificação da JVM, no entanto, não possui essa restrição; portanto, um arquivo de classe pode conter dois métodos, simplesmente não há como produzir um arquivo de classe usando o compilador Java normal. Há um bom exemplo / explicação nesta resposta .
fonte
Thread.stop(Throwable)
para um lançamento sorrateiro. Presumo que o já vinculado seja mais rápido.GOTO
pode ser usado com etiquetas para criar suas próprias estruturas de controle (além defor
while
etc)this
variável local dentro de um métodoComo um ponto relacionado, você pode obter o nome do parâmetro para métodos se compilado com depuração (o Paranamer faz isso lendo o bytecode
fonte
override
esta variável local?this
variável possui índice zero, mas além de ser pré-inicializada com athis
referência ao inserir um método de instância, é apenas uma variável local. Assim, você pode escrever um valor diferente, que pode agir como finalizaçãothis
do escopo ou alteração dathis
variável, dependendo de como você o usa.this
pode ser reatribuído? Eu acho que foi apenas a substituição da palavra que me fez pensar no que isso significava exatamente.Talvez a seção 7A deste documento seja interessante, embora se trate de armadilhas de códigos de bytes, e não de recursos de códigos de código .
fonte
Na linguagem Java, a primeira instrução em um construtor deve ser uma chamada para o construtor de superclasse. O bytecode não possui essa limitação; em vez disso, a regra é que o construtor da superclasse ou outro construtor da mesma classe deve ser chamado para o objeto antes de acessar os membros. Isso deve permitir mais liberdade, como:
Eu não os testei, por favor, corrija-me se estiver errado.
fonte
Algo que você pode fazer com o código de bytes, em vez do código Java simples, é gerar código que pode ser carregado e executado sem um compilador. Muitos sistemas têm JRE em vez de JDK e se você deseja gerar código dinamicamente, pode ser melhor, se não mais fácil, gerar código de bytes em vez de código Java, que deve ser compilado antes de poder ser usado.
fonte
Escrevi um otimizador de bytecode quando eu era um I-Play (ele foi projetado para reduzir o tamanho do código para aplicativos J2ME). Um recurso que eu adicionei foi a capacidade de usar bytecode embutido (semelhante à linguagem assembly embutida em C ++). Consegui reduzir o tamanho de uma função que fazia parte de um método de biblioteca usando a instrução DUP, pois preciso do valor duas vezes. Eu também tinha instruções de zero byte (se você está chamando um método que usa um char e deseja passar um int, que você sabe que não precisa ser convertido, adicionei int2char (var) para substituir char (var) e ele removeria a instrução i2c para reduzir o tamanho do código.Eu também fiz float a = 2.3; float b = 3.4; float c = a + b; e isso seria convertido em ponto fixo (mais rápido, e também alguns J2ME não suporte ponto flutuante).
fonte
Em Java, se você tentar substituir um método público por um método protegido (ou qualquer outra redução no acesso), receberá um erro: "ao tentar atribuir privilégios de acesso mais fracos". Se você fizer isso com o bytecode da JVM, o verificador estará bem com ele e poderá chamar esses métodos através da classe pai como se fossem públicos.
fonte