¿Existen actualmente (Java 6) cosas que puede hacer en el código de bytes de Java que no puede hacer desde el lenguaje Java?
Sé que ambos están completos en Turing, así que lea "puede hacer" como "puede hacerlo significativamente más rápido / mejor, o simplemente de una manera diferente".
Estoy pensando en códigos de bytes adicionales como invokedynamic
, que no se pueden generar usando Java, excepto que uno específico es para una versión futura.
rol
en el ensamblador, que no puedes escribir en C ++.(x<<n)|(x>>(32-n))
a unarol
instrucción.Respuestas:
Hasta donde sé, no hay características principales en los códigos de bytes compatibles con Java 6 a las que no se pueda acceder también desde el código fuente de Java. La razón principal de esto es obviamente que el bytecode de Java fue diseñado con el lenguaje Java en mente.
Sin embargo, hay algunas características que no son producidas por los compiladores modernos de Java:
La
ACC_SUPER
bandera :Este es un indicador que se puede establecer en una clase y especifica cómo
invokespecial
se maneja un caso de esquina específico del código de bytes para esta clase. Está configurado por todos los compiladores de Java modernos (donde "moderno" es> = Java 1.1, si no recuerdo mal) y solo los compiladores de Java antiguos producían archivos de clase donde esto no estaba configurado. Este indicador existe solo por razones de compatibilidad con versiones anteriores. Tenga en cuenta que a partir de Java 7u51, ACC_SUPER se ignora por completo por razones de seguridad.Los
jsr
/ret
bytecodes.Estos bytecodes se usaron para implementar subrutinas (principalmente para implementar
finally
bloques). Ya no se producen desde Java 6 . La razón de su desaprobación es que complican mucho la verificación estática sin una gran ganancia (es decir, el código que usa casi siempre se puede volver a implementar con saltos normales con muy poca sobrecarga).Tener dos métodos en una clase que solo difieren en el tipo de retorno.
La especificación del lenguaje Java no permite dos métodos en la misma clase cuando difieren solo en su tipo de retorno (es decir, el mismo nombre, la misma lista de argumentos, ...). Sin embargo, la especificación JVM no tiene esa restricción, por lo que un archivo de clase puede contener dos de estos métodos, simplemente no hay forma de producir dicho archivo de clase utilizando el compilador Java normal. Hay un buen ejemplo / explicación en esta respuesta .
fuente
a
otraA
dentro del archivo JAR. Me tomó alrededor de media hora descomprimir en una máquina Windows antes de darme cuenta de dónde estaban las clases faltantes. :)'.'
,';'
,'['
, o'/'
. Los nombres de los métodos son los mismos, pero tampoco pueden contener'<'
o'>'
. (Con las notables excepciones de<init>
y,<clinit>
por ejemplo, y los constructores estáticos). Debo señalar que si sigue estrictamente la especificación, los nombres de clase en realidad están mucho más restringidos, pero las restricciones no se aplican."throws ex1, ex2, ..., exn"
como parte de las firmas de métodos; No puede agregar cláusulas de excepción a los métodos anulados. PERO, a la JVM no podría importarle menos. Por lo tanto, solo losfinal
métodos están realmente garantizados por la JVM para estar libres de excepciones, aparte deRuntimeException
syError
s, por supuesto. Demasiado para el manejo de excepciones comprobadas: DDespués de trabajar con código de bytes de Java durante bastante tiempo e investigar un poco más sobre este asunto, aquí hay un resumen de mis hallazgos:
Ejecute código en un constructor antes de llamar a un súper constructor o constructor auxiliar
En el lenguaje de programación Java (JPL), la primera declaración de un constructor debe ser una invocación de un superconstructor u otro constructor de la misma clase. Esto no es cierto para el código de bytes de Java (JBC). Dentro del código de bytes, es absolutamente legítimo ejecutar cualquier código antes de un constructor, siempre que:
Establecer campos de instancia antes de llamar a un súper constructor o constructor auxiliar
Como se mencionó anteriormente, es perfectamente legal establecer un valor de campo de una instancia antes de llamar a otro constructor. Incluso existe un truco heredado que le permite explotar esta "característica" en las versiones de Java anteriores a la 6:
De esta forma, se podría establecer un campo antes de que se invoque el súper constructor, lo que sin embargo ya no es posible. En JBC, este comportamiento aún se puede implementar.
Ramifica una llamada de súper constructor
En Java, no es posible definir una llamada de constructor como
Sin embargo, hasta Java 7u23, el verificador de HotSpot VM no realizó esta comprobación, por lo que fue posible. Esto fue utilizado por varias herramientas de generación de código como una especie de pirateo, pero ya no es legal implementar una clase como esta.Este último fue simplemente un error en esta versión del compilador. En las versiones más recientes del compilador, esto es nuevamente posible.
Definir una clase sin ningún constructor.
El compilador de Java siempre implementará al menos un constructor para cualquier clase. En el código de bytes de Java, esto no es obligatorio. Esto permite la creación de clases que no se pueden construir incluso cuando se usa la reflexión. Sin embargo, el uso
sun.misc.Unsafe
aún permite la creación de tales instancias.Definir métodos con firma idéntica pero con diferente tipo de retorno
En el JPL, un método se identifica como único por su nombre y sus tipos de parámetros sin formato. En JBC, el tipo de retorno sin procesar también se considera.
Defina campos que no difieran por nombre sino solo por tipo
Un archivo de clase puede contener varios campos del mismo nombre siempre que declaren un tipo de campo diferente. La JVM siempre se refiere a un campo como una tupla de nombre y tipo.
Lanza excepciones marcadas no declaradas sin atraparlas
El tiempo de ejecución de Java y el código de bytes de Java no conocen el concepto de excepciones comprobadas. Es solo el compilador de Java que verifica que las excepciones marcadas siempre se detectan o declaran si se lanzan.
Use la invocación de métodos dinámicos fuera de las expresiones lambda
La llamada invocación del método dinámico se puede usar para cualquier cosa, no solo para las expresiones lambda de Java. El uso de esta característica permite, por ejemplo, cambiar la lógica de ejecución en tiempo de ejecución. Muchos lenguajes de programación dinámicos que se reducen a JBC mejoraron su rendimiento al usar esta instrucción. En el código de bytes de Java, también podría emular expresiones lambda en Java 7 donde el compilador aún no permitía el uso de la invocación de métodos dinámicos mientras la JVM ya entendía la instrucción.
Utilice identificadores que normalmente no se consideran legales
¿Alguna vez te ha gustado usar espacios y un salto de línea en el nombre de tu método? Cree su propio JBC y buena suerte para la revisión del código. Los únicos caracteres no válidos para identificadores son
.
,;
,[
y/
. Además, los métodos que no tienen nombre<init>
o<clinit>
que no pueden contener<
y>
.Reasignar
final
parámetros o lathis
referenciafinal
los parámetros no existen en JBC y, en consecuencia, pueden reasignarse. Cualquier parámetro, incluida lathis
referencia, solo se almacena en una matriz simple dentro de la JVM, lo que permite reasignar lathis
referencia en el índice0
dentro de un marco de método único.Reasignar
final
camposSiempre que se asigne un campo final dentro de un constructor, es legal reasignar este valor o incluso no asignar un valor en absoluto. Por lo tanto, los siguientes dos constructores son legales:
Para los
static final
campos, incluso está permitido reasignar los campos fuera del inicializador de clase.Trate a los constructores y al inicializador de clase como si fueran métodos
Esto es más una característica conceptual, pero los constructores no reciben un trato diferente dentro de JBC que los métodos normales. Es solo el verificador de JVM lo que asegura que los constructores llamen a otro constructor legal. Aparte de eso, es simplemente una convención de nomenclatura de Java que se debe llamar a los constructores
<init>
y que se llama al inicializador de clase<clinit>
. Además de esta diferencia, la representación de métodos y constructores es idéntica. Como Holger señaló en un comentario, incluso puede definir constructores con tipos de retorno distintosvoid
o un inicializador de clase con argumentos, aunque no es posible llamar a estos métodos.Crear registros asimétricos * .
Al crear un registro
javac generará un archivo de clase con un solo campo llamado
bar
, un método de acceso llamadobar()
y un constructor tomando un soloObject
. Además,bar
se agrega un atributo de registro para . Al generar manualmente un registro, es posible crear una forma de constructor diferente, omitir el campo e implementar el descriptor de acceso de manera diferente. Al mismo tiempo, todavía es posible hacer que la API de reflexión crea que la clase representa un registro real.Llame a cualquier súper método (hasta Java 1.1)
Sin embargo, esto solo es posible para las versiones Java 1 y 1.1. En JBC, los métodos siempre se envían en un tipo de destino explícito. Esto significa que para
fue posible implementar
Qux#baz
para invocarFoo#baz
mientras saltabaBar#baz
. Si bien aún es posible definir una invocación explícita para llamar a otra implementación de súper método que la de la súper clase directa, esto ya no tiene ningún efecto en las versiones de Java posteriores a 1.1. En Java 1.1, este comportamiento se controlaba configurando elACC_SUPER
indicador que permitiría el mismo comportamiento que solo llama a la implementación directa de la superclase.Definir una llamada no virtual de un método que se declara en la misma clase
En Java, no es posible definir una clase
El código anterior siempre dará lugar a una
RuntimeException
cuandofoo
se invoca en una instancia deBar
. No es posible definir elFoo::foo
método para invocar su propiobar
método que se define enFoo
. Comobar
es un método de instancia no privado, la llamada siempre es virtual. Sin embargo, con el código de bytes, se puede definir la invocación para usar elINVOKESPECIAL
código de operación que vincula directamente labar
llamada al métodoFoo::foo
aFoo
la versión. Este código de operación se usa normalmente para implementar invocaciones de súper métodos, pero puede reutilizar el código de operación para implementar el comportamiento descrito.Anotaciones de grano fino
En Java, las anotaciones se aplican de acuerdo con lo
@Target
que declaran las anotaciones. Mediante la manipulación de código de bytes, es posible definir anotaciones independientemente de este control. Además, por ejemplo, es posible anotar un tipo de parámetro sin anotar el parámetro, incluso si la@Target
anotación se aplica a ambos elementos.Definir cualquier atributo para un tipo o sus miembros.
Dentro del lenguaje Java, solo es posible definir anotaciones para campos, métodos o clases. En JBC, básicamente puede incrustar cualquier información en las clases de Java. Sin embargo, para utilizar esta información, ya no puede confiar en el mecanismo de carga de clases de Java, sino que necesita extraer la metainformación usted mismo.
Desbordamiento e implícitamente asignar
byte
,short
,char
yboolean
los valoresLos últimos tipos primitivos no se conocen normalmente en JBC, pero solo se definen para tipos de matriz o para descriptores de campo y método. Dentro de las instrucciones del código de bytes, todos los tipos nombrados ocupan el espacio de 32 bits que permite representarlos como
int
. Oficialmente, sólo losint
,float
,long
ydouble
existen tipos del código de bytes, que todos necesitamos conversión explícita por la regla de verificador de la JVM.No liberar un monitor
Un
synchronized
bloque en realidad está compuesto por dos declaraciones, una para adquirir y otra para liberar un monitor. En JBC, puede adquirir uno sin liberarlo.Nota : En implementaciones recientes de HotSpot, esto lleva a un
IllegalMonitorStateException
final de un método o a una versión implícita si el método se termina por una excepción en sí misma.Agregar más de una
return
declaración a un inicializador de tipoEn Java, incluso un inicializador de tipo trivial como
es ilegal. En el código de bytes, el inicializador de tipo se trata como cualquier otro método, es decir, las declaraciones de retorno se pueden definir en cualquier lugar.
Crea bucles irreducibles
El compilador de Java convierte bucles en sentencias goto en código de bytes Java. Dichas declaraciones pueden usarse para crear bucles irreducibles, lo que el compilador de Java nunca hace.
Define un bloque de captura recursivo
En el código de bytes de Java, puede definir un bloque:
Una declaración similar se crea implícitamente cuando se usa un
synchronized
bloque en Java donde cualquier excepción al liberar un monitor vuelve a las instrucciones para liberar este monitor. Normalmente, no debería producirse ninguna excepción en dicha instrucción, pero si así fuera (por ejemplo, en desusoThreadDeath
), el monitor aún se liberaría.Llamar a cualquier método predeterminado
El compilador de Java requiere que se cumplan varias condiciones para permitir la invocación de un método predeterminado:
B
extiende la interfazA
pero no anula un métodoA
, aún se puede invocar el método.Para el código de bytes de Java, solo cuenta la segunda condición. Sin embargo, el primero es irrelevante.
Invocar un súper método en una instancia que no sea
this
El compilador de Java solo permite invocar un método super (o el predeterminado de la interfaz) en instancias de
this
. Sin embargo, en el código de bytes, también es posible invocar el súper método en una instancia del mismo tipo similar al siguiente:Acceder a miembros sintéticos
En el código de bytes de Java, es posible acceder a miembros sintéticos directamente. Por ejemplo, considere cómo en el siguiente ejemplo
Bar
se accede a la instancia externa de otra instancia:Esto es generalmente cierto para cualquier campo sintético, clase o método.
Definir información de tipo genérico fuera de sincronización
Si bien el tiempo de ejecución de Java no procesa tipos genéricos (después de que el compilador de Java aplica la eliminación de tipos), esta información todavía se adjunta a una clase compilada como metainformación y se hace accesible a través de la API de reflexión.
El verificador no verifica la consistencia de estos
String
valores codificados con metadatos. Por lo tanto, es posible definir información sobre tipos genéricos que no coincida con el borrado. Como consecuencia, las siguientes afirmaciones pueden ser ciertas:Además, la firma se puede definir como no válida de modo que se genere una excepción de tiempo de ejecución. Esta excepción se produce cuando se accede a la información por primera vez, ya que se evalúa perezosamente. (Similar a los valores de anotación con un error).
Agregar metainformación de parámetros solo para ciertos métodos
El compilador de Java permite incrustar el nombre del parámetro y la información del modificador al compilar una clase con el
parameter
indicador habilitado. Sin embargo, en el formato de archivo de clase Java, esta información se almacena por método, lo que hace posible incrustar solo dicha información de método para ciertos métodos.Arruine las cosas y bloquea tu JVM
Como ejemplo, en el código de bytes de Java, puede definir invocar cualquier método en cualquier tipo. Por lo general, el verificador se quejará si un tipo no conoce dicho método. Sin embargo, si invoca un método desconocido en una matriz, encontré un error en alguna versión de JVM donde el verificador se perderá esto y su JVM finalizará una vez que se invoque la instrucción. Sin embargo, esto no es una característica, pero técnicamente es algo que no es posible con Java compilado javac . Java tiene algún tipo de doble validación. La primera validación es aplicada por el compilador de Java, la segunda por la JVM cuando se carga una clase. Al omitir el compilador, puede encontrar un punto débil en la validación del verificador. Sin embargo, esta es más una declaración general que una característica.
Anotar el tipo de receptor de un constructor cuando no hay clase externa
Desde Java 8, los métodos no estáticos y los constructores de clases internas pueden declarar un tipo de receptor y anotar estos tipos. Los constructores de clases de nivel superior no pueden anotar su tipo de receptor, ya que la mayoría no declaran uno.
Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
Sin embargo, dado que devuelve unaAnnotatedType
representaciónFoo
, es posible incluir anotaciones de tipo paraFoo
el constructor directamente en el archivo de clase donde la API de reflexión luego lee estas anotaciones.Usar instrucciones de código de bytes no utilizados / heredados
Como otros lo nombraron, lo incluiré también. Anteriormente, Java hacía uso de subrutinas por las declaraciones
JSR
yRET
. JBC incluso conocía su propio tipo de dirección de retorno para este propósito. Sin embargo, el uso de subrutinas complicaba demasiado el análisis de código estático, por lo que estas instrucciones ya no se utilizan. En cambio, el compilador de Java duplicará el código que compila. Sin embargo, esto básicamente crea una lógica idéntica, por lo que realmente no considero que logre algo diferente. Del mismo modo, podría agregar, por ejemplo, elNOOP
instrucción de código de bytes que tampoco es utilizada por el compilador de Java, pero esto tampoco le permitiría lograr algo nuevo tampoco. Como se señaló en el contexto, estas "instrucciones de características" mencionadas ahora se eliminan del conjunto de códigos de operación legales, lo que las hace aún menos características.fuente
<clinit>
método definiendo métodos con el nombre<clinit>
pero aceptando parámetros o teniendo unvoid
tipo sin retorno. Pero estos métodos no son muy útiles, la JVM los ignorará y el código de bytes no podrá invocarlos. El único uso sería confundir a los lectores.IllegalMonitorStateException
mensaje si omitió lamonitorexit
instrucción. Y en caso de una salida de método excepcional que no pudo hacer unmonitorexit
, restablece el monitor en silencio.Aquí hay algunas características que se pueden hacer en código de bytes de Java pero no en el código fuente de Java:
Lanzar una excepción marcada de un método sin declarar que el método la arroja. Las excepciones marcadas y no marcadas son algo que solo es verificado por el compilador de Java, no por la JVM. Debido a esto, por ejemplo, Scala puede lanzar excepciones comprobadas de los métodos sin declararlas. Aunque con los genéricos de Java hay una solución alternativa llamada furtivo .
Tener dos métodos en una clase que solo difieren en el tipo de retorno, como ya se mencionó en la respuesta de Joachim : La especificación del lenguaje Java no permite dos métodos en la misma clase cuando difieren solo en su tipo de retorno (es decir, el mismo nombre, la misma lista de argumentos, ...) Sin embargo, la especificación JVM no tiene esa restricción, por lo que un archivo de clase puede contener dos de estos métodos, simplemente no hay forma de producir dicho archivo de clase utilizando el compilador Java normal. Hay un buen ejemplo / explicación en esta respuesta .
fuente
Thread.stop(Throwable)
para un lanzamiento furtivo. Sin embargo, supongo que el que ya está vinculado es más rápido.GOTO
puede usarse con etiquetas para crear sus propias estructuras de control (que no seanfor
while
etc.)this
variable local dentro de un métodoComo punto relacionado, puede obtener el nombre del parámetro para los métodos si se compila con depuración ( Paranamer hace esto leyendo el código de bytes
fuente
override
la variable local?this
variable tiene índice cero, pero además de preinicializarse con lathis
referencia al ingresar un método de instancia, es solo una variable local. Por lo tanto, puede escribirle un valor diferente, que puede actuar como terminarthis
'alcance o cambiar lathis
variable, dependiendo de cómo lo use.this
se puede reasignar? Creo que fue solo la anulación de la palabra lo que me hizo preguntarme qué significaba exactamente.Tal vez la sección 7A de este documento sea de interés, aunque se trata de errores de bytecode en lugar de características de bytecode .
fuente
En lenguaje Java, la primera instrucción en un constructor debe ser una llamada al constructor de superclase. Bytecode no tiene esta limitación, en cambio, la regla es que se debe llamar al constructor de la superclase u otro constructor de la misma clase para el objeto antes de acceder a los miembros. Esto debería permitir más libertad como:
No los he probado, así que corrígeme si me equivoco.
fuente
Algo que puede hacer con el código de bytes, en lugar del código Java simple, es generar código que se pueda cargar y ejecutar sin un compilador. Muchos sistemas tienen JRE en lugar de JDK y si desea generar código dinámicamente, puede ser mejor, si no más fácil, generar código de bytes en lugar de código Java que debe compilarse antes de que pueda usarse.
fuente
Escribí un optimizador de bytecode cuando era un I-Play (fue diseñado para reducir el tamaño del código para aplicaciones J2ME). Una característica que agregué fue la capacidad de usar bytecode en línea (similar al lenguaje ensamblador en línea en C ++). Logré reducir el tamaño de una función que formaba parte de un método de biblioteca utilizando la instrucción DUP, ya que necesito el valor dos veces. También tenía instrucciones de cero bytes (si está llamando a un método que toma un char y desea pasar un int, que sabe que no necesita ser lanzado, agregué int2char (var) para reemplazar char (var) y eliminaría la instrucción i2c para reducir el tamaño del código. También hice que flotara a = 2.3; flotante b = 3.4; flotante c = a + b; y eso se convertiría en un punto fijo (más rápido, y también algunos J2ME no lo hicieron) Soporte de coma flotante).
fuente
En Java, si intenta anular un método público con un método protegido (o cualquier otra reducción en el acceso), obtiene un error: "intentando asignar privilegios de acceso más débiles". Si lo hace con el código de bytes JVM, el verificador está bien y puede llamar a estos métodos a través de la clase padre como si fueran públicos.
fuente