Pregunta Las características de bytecode no están disponibles en el lenguaje Java


¿Hay actualmente (Java 6) cosas que pueda hacer en bytecode de Java que no pueda hacer desde el lenguaje Java?

Sé que ambos están completos, así que lea "puedo hacer" ya que "puedo hacer mucho más rápido / mejor, o simplemente de una manera diferente".

Estoy pensando en códigos de bytes adicionales como invokedynamic, que no se puede generar utilizando Java, excepto que el específico es para una versión futura.


138
2017-07-26 08:39


origen


Respuestas:


Hasta donde sé, no hay características principales en los códigos de bytes admitidos por Java 6 que tampoco son accesibles 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 de Java modernos:

  • los ACC_SUPER bandera:

    Esta es una bandera que se puede establecer en una clase y especifica cómo un caso de esquina específico de la invokespecial bytecode se maneja para esta clase. Está establecido por todos los compiladores modernos de Java (donde "moderno" es> = Java 1.1, si no recuerdo mal) y solo los antiguos compiladores de Java producían archivos de clase en los que esto no estaba establecido. Esta marca existe solo por razones de compatibilidad con versiones anteriores. Tenga en cuenta que a partir de Java 7u51, ACC_SUPER se ignora por completo debido a razones de seguridad.

  • los jsr/ret bytecodes.

    Estos códigos de bytes se usaron para implementar sub-rutinas (principalmente para implementar finally bloques). Son ya no se produce desde Java 6. La razón de su desaprobación es que complican mucho la verificación estática sin gran ganancia (es decir, el código que se utiliza 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 devolución.

    La especificación del lenguaje Java no permite dos métodos en la misma clase cuando difieren solamente en su tipo de devolución (es decir, el mismo nombre, la misma lista de argumentos, ...). Sin embargo, la especificación de JVM no tiene tal restricción, por lo que un archivo de clase poder contienen 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.


58
2018-04-22 11:41



Después de trabajar con el código de bytes de Java durante bastante tiempo y hacer algunas investigaciones adicionales sobre este asunto, aquí hay un resumen de mis hallazgos:

Ejecutar 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 super constructor u otro constructor de la misma clase. Esto no es cierto para el código de bytes Java (JBC). Dentro del código de bytes, es absolutamente legítimo ejecutar cualquier código antes de un constructor, siempre que:

  • Se llama a otro constructor compatible en algún momento después de este bloque de código.
  • Esta llamada no está dentro de una declaración condicional.
  • Antes de esta llamada al constructor, no se lee ningún campo de la instancia construida y no se invoca ninguno de sus métodos. Esto implica el siguiente artículo.

Establecer campos de instancia antes de llamar a un super 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 6:

class Foo {
  public String s;
  public Foo() {
    System.out.println(s);
  }
}

class Bar extends Foo {
  public Bar() {
    this(s = "Hello World!");
  }
  private Bar(String helper) {
    super();
  }
}

De esta manera, un campo podría establecerse antes de que se invoque el superconstructor, que, sin embargo, ya no es posible. En JBC, este comportamiento aún se puede implementar.

Branch una llamada súper constructor

En Java, no es posible definir una llamada de constructor como

class Foo {
  Foo() { }
  Foo(Void v) { }
}

class Bar() {
  if(System.currentTimeMillis() % 2 == 0) {
    super();
  } else {
    super(null);
  }
}

Hasta Java 7u23, el verificador de la máquina virtual de HotSpot sin embargo omitió esta comprobación y es por eso que fue posible. Esto fue utilizado por varias herramientas de generación de código como una especie de truco, 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 de compilador más nuevas, esto nuevamente es 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 utiliza la reflexión. Sin embargo, usando sun.misc.Unsafe todavía permite la creación de tales instancias.

Definir métodos con firma idéntica pero con diferente tipo de devolución

En el JPL, un método se identifica como único por su nombre y sus tipos de parámetros brutos. En JBC, el tipo de devolución sin procesar también se considera.

Defina campos que no difieren por nombre pero solo por tipo

Un archivo de clase puede contener varios campos del mismo nombre siempre que declare un tipo de campo diferente. La JVM siempre se refiere a un campo como una tupla de nombre y tipo.

Lanza excepciones comprobadas no declaradas sin capturarlas

El tiempo de ejecución de Java y el código de bytes de Java no conocen el concepto de excepciones comprobadas. Solo el compilador de Java verifica que las excepciones comprobadas siempre se capturan o declaran si se lanzan.

Usar la invocación de método dinámico fuera de las expresiones lambda

La llamada invocación de método dinámico se puede usar para cualquier cosa, no solo para las expresiones lambda de Java. El uso de esta función permite, por ejemplo, cambiar la lógica de ejecución en el tiempo de ejecución. Muchos lenguajes de programación dinámicos que se reducen a JBC mejoró su rendimiento usando esta instrucción. En el código de bytes Java, también podría emular las 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 las instrucciones.

Use identificadores que normalmente no se consideran legales

¿Alguna vez imaginó usar espacios y un salto de línea en el nombre de su método? Crea tu propio JBC y buena suerte para la revisión del código. Los únicos caracteres ilegales para los identificadores son ., ;, [ y /. Además, los métodos que no son nombrados <init> o <clinit> no puede contener <y >.

Reasignar final parámetros o el this referencia

final los parámetros no existen en JBC y, por lo tanto, pueden reasignarse. Cualquier parámetro, incluido el this la referencia solo se almacena en una matriz simple dentro de la JVM, lo que permite reasignar el this referencia en el índice 0 dentro de un solo marco de método.

Reasignar final campos

Siempre que se asigne un campo final dentro de un constructor, es legal reasignar este valor o incluso no asignar ningún valor. Por lo tanto, los siguientes dos constructores son legales:

class Foo {
  final int bar;
  Foo() { } // bar == 0
  Foo(Void v) { // bar == 2
    bar = 1;
    bar = 2;
  }
}

por static final campos, incluso se le permite reasignar los campos fuera de el inicializador de clase.

Trate a los constructores y al inicializador de clase como si fueran métodos

Esto es más de un característica concepcional pero los constructores no son tratados de manera diferente dentro de JBC que los métodos normales. Solo el verificador de JVM asegura que los constructores llamen a otro constructor legal. Aparte de eso, es simplemente una convención de nomenclatura de Java que los constructores deben ser llamados <init> y que el inicializador de clase se llama <clinit>. Además de esta diferencia, la representación de métodos y constructores es idéntica. Como señaló Holger en un comentario, incluso puede definir constructores con tipos de retorno distintos de void o un inicializador de clase con argumentos, aunque no es posible llamar a estos métodos.

Llamar a cualquier método super (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 objetivo explícito. Esto significa que para

class Foo {
  void baz() { System.out.println("Foo"); }
}

class Bar extends Foo {
  @Override
  void baz() { System.out.println("Bar"); }
}

class Qux extends Bar {
  @Override
  void baz() { System.out.println("Qux"); }
}

fue posible implementar Qux#baz invocar Foo#baz mientras saltas sobre Bar#baz. Si bien aún es posible definir una invocación explícita para llamar a otra implementación de supermétodo que la de la superclase directa, esto ya no tiene ningún efecto en las versiones de Java posteriores a 1.1. En Java 1.1, este comportamiento fue controlado estableciendo el ACC_SUPER bandera que permitiría el mismo comportamiento que solo llama a la implementación de la superclase directa.

Definir una llamada no virtual de un método declarado en la misma clase

En Java, no es posible definir una clase

class Foo {
  void foo() {
    bar();
  }
  void bar() { }
}

class Bar extends Foo {
  @Override void bar() {
    throw new RuntimeException();
  }
}

El código anterior siempre dará como resultado un RuntimeException cuando foo se invoca en una instancia de Bar. No es posible definir el Foo::foo método para invocar su propio  bar método que se define en Foo. Como bar es un método de instancia no privada, la llamada es siempre virtual. Con el código de bytes, uno puede definir la invocación para usar el INVOKESPECIAL opcode que vincula directamente el bar llamada de método en Foo::foo a Foola versión de Este código de operación se usa normalmente para implementar invocaciones de supermétodo, pero puede reutilizar el código de operación para implementar el comportamiento descrito.

Anotaciones de tipo de grano fino

En Java, las anotaciones se aplican según su @Target que las anotaciones declaran. Usando la manipulación de código de byte, es posible definir anotaciones independientemente de este control. Además, es posible, por ejemplo, anotar un tipo de parámetro sin anotar el parámetro, incluso si el @Targetla 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 puedes insertar 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, pero necesita extraer la metainformación usted mismo.

Desbordamiento e implícitamente asignar byte, short, chary boolean valores

Los ú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 toman el espacio de 32 bits que permite representarlos como int. Oficialmente, solo el int, float, longy double existen tipos dentro del código de bytes que todos necesitan conversión explícita por la regla del verificador de la JVM.

No lanzar un monitor

UN synchronized en realidad, el bloque está compuesto por dos instrucciones, una para adquirir y otra para lanzar un monitor. En JBC, puedes adquirir uno sin soltarlo.

Nota: En implementaciones recientes de HotSpot, esto en cambio conduce a una IllegalMonitorStateException al final de un método o a una versión implícita si el método es terminado por una excepción en sí misma.

Agrega más de uno return declaración a un inicializador de tipo

En Java, incluso un inicializador de tipo trivial como

class Foo {
  static {
    return;
  }
}

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.

Crear bucles irreducibles

El compilador de Java convierte bucles en goto en código de bytes Java. Tales declaraciones se pueden usar para crear bucles irreducibles, lo que el compilador de Java nunca hace.

Definir un bloque de catch recursivo

En el código de bytes de Java, puede definir un bloque:

try {
  throw new Exception();
} catch (Exception e) {
  <goto on exception>
  throw Exception();
}

Una declaración similar se crea implícitamente cuando se utiliza una synchronized bloquear en Java donde cualquier excepción al liberar un monitor vuelve a las instrucciones para liberar este monitor. Normalmente, no debería haber ninguna excepción en tal instrucción, pero si lo hiciera (por ejemplo, la obsolescencia ThreadDeath), el monitor aún sería lanzado.

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:

  1. El método debe ser el más específico (no debe ser anulado por una interfaz secundaria implementada por alguna tipo, incluidos los tipos súper).
  2. El tipo de interfaz del método predeterminado debe ser implementado directamente por la clase que llama al método predeterminado. Sin embargo, si la interfaz B extiende la interfaz A pero no anula un método en A, el método todavía puede ser invocado.

Para el código de bytes de Java, solo la segunda condición cuenta. El primero es sin embargo irrelevante.

Invocar un supermétodo en una instancia que no es this

El compilador de Java solo permite invocar un método super (o interfaz predeterminada) en instancias de this. En el código de bytes, sin embargo, también es posible invocar el método super en una instancia del mismo tipo similar a la siguiente:

class Foo {
  void m(Foo f) {
    f.super.toString(); // calls Object::toString
  }
  public String toString() {
    return "foo";
  }
}

Acceso 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 la instancia externa de otro Bar se accede a la instancia:

class Foo {
  class Bar { 
    void bar(Bar bar) {
      Foo foo = bar.Foo.this;
    }
  }
}

Esto es generalmente cierto para cualquier campo sintético, clase o método.

Definir información de tipo genérico fuera de sincronización

Mientras que el tiempo de ejecución de Java no procesa los tipos genéricos (después de que el compilador de Java se aplica el tipo de borrado), esta información es todavía attcheched a una clase compilada como meta información y se hace accesible a través de la API de reflexión.

El verificador no verifica la consistencia de estos metadatos Stringvalores codificados Por lo tanto, es posible definir información sobre tipos genéricos que no coincida con el borrado. Como una consecuencia, las siguientes afirmaciones pueden ser ciertas:

Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());

Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);

Además, la firma se puede definir como no válida de manera que se genera 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)

Anexar 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 bandera habilitada En el formato de archivo de clase Java, esta información se almacena sin embargo por método lo que hace posible embeber, únicamente las informaciones método para ciertos métodos.

Desordenar las cosas y dañar 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 se invoca un método desconocido en una matriz, He encontrado un fallo en alguna versión de JVM en el que el verificador se perderá esto y su JVM terminar una vez que se invoca la instrucción. Sin embargo, esto no es una característica, pero técnicamente es algo que no es posible con javac compilado Java. 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, esto es más una afirmación general que una característica.

Anotar el tipo de receptor del 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 declara uno.

class Foo {
  class Bar {
    Bar(@TypeAnnotation Foo Foo.this) { }
  }
  Foo() { } // Must not declare a receiver type
}

Ya que Foo.class.getDeclaredConstructor().getAnnotatedReceiverType() sin embargo devuelve un AnnotatedType representando Foo, es posible incluir anotaciones tipo para Foo's constructor directamente en el archivo de clase donde estas anotaciones son leídas más tarde por la API de reflexión.

Usar instrucciones de código de bytes heredadas / no utilizadas

Como otros lo nombraron, lo incluiré también. Anteriormente Java hacía uso de subrutinas por el JSRy RETdeclaraciones. JBC incluso conocía su propio tipo de dirección de retorno para este propósito. Sin embargo, el uso de subrutinas complicó en exceso el análisis de código estático, por lo que estas instrucciones ya no se usan. En cambio, el compilador de Java duplicará el código que compila. Sin embargo, esto básicamente crea una lógica idéntica y es por eso que realmente no considero que logre algo diferente. Del mismo modo, podría, por ejemplo, agregar el NOOP la instrucción de código de bytes que no es utilizada por el compilador de Java tampoco, pero esto tampoco le permitirá lograr algo nuevo. Como se señala en el contexto, estas "instrucciones de funciones" mencionadas ahora se eliminan del conjunto de códigos de operación legales, lo que los hace aún menos característicos.


384
2017-07-26 09:55



Aquí hay algunas características que se pueden hacer en bytecode de Java pero no en código fuente de Java:

  • Lanzar una excepción marcada de un método sin declarar que el método lo arroja. Las excepciones marcadas y no revisadas son algo que solo se comprueba mediante el compilador de Java, no la JVM. Debido a esto, por ejemplo, Scala puede arrojar excepciones comprobadas de los métodos sin declararlos. Aunque con los genéricos de Java hay una solución llamada tiro furtivo.

  • Tener dos métodos en una clase que solo difieren en el tipo de devolución, 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 solamente en su tipo de devolución (es decir, el mismo nombre, la misma lista de argumentos, ...). Sin embargo, la especificación de JVM no tiene tal restricción, por lo que un archivo de clase poder contienen 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.


12
2018-02-14 16:50



  • GOTO se puede usar con etiquetas para crear sus propias estructuras de control (que no sean for  while etc)
  • Puede anular el this variable local dentro de un método
  • Combinando ambos puedes crear un bytecode optimizado para crear una cola de espera (hago esto en JCompilo)

Como punto relacionado, puede obtener el nombre del parámetro para los métodos si se compila con depuración (Paranamer hace esto leyendo el bytecode


6
2017-07-26 09:19



Tal vez la sección 7A en este documento es de interés, aunque se trata de bytecode trampas en lugar de bytecode caracteristicas.


4
2017-08-08 06:56



En lenguaje Java, la primera declaración en un constructor debe ser una llamada al constructor de la superclase. Bytecode no tiene esta limitación, en cambio, la regla es que el constructor de la superclase u otro constructor de la misma clase debe invocarse para el objeto antes de acceder a los miembros. Esto debería permitir más libertad, como:

  • Cree una instancia de otro objeto, almacénelo en una variable local (o pila) y páselo como un parámetro al constructor de la superclase mientras mantiene la referencia en esa variable para otro uso.
  • Llamar a otros constructores diferentes en función de una condición. Esto debería ser posible: ¿Cómo llamar a un constructor diferente condicionalmente en Java?

No los he probado, así que por favor corrígeme si me equivoco.


3
2017-07-26 09:11



Algo que puede hacer con el código de bytes, en lugar del código Java simple, es generar código que puede cargarse y ejecutarse sin un compilador. Muchos sistemas tienen JRE en lugar de JDK y, si desea generar código de forma dinámica, puede ser mejor, si no más fácil, generar código de bytes en lugar de compilar el código de Java antes de poder utilizarlo.


2
2018-04-17 15:48