java - ¿Es "final" final en tiempo de ejecución?
java-bytecode-asm (3)
He estado jugando con ASM , y creo que logré agregar el modificador final a un campo de instancia de una clase; sin embargo, luego procedí a crear una instancia de dicha clase e invocar un configurador en ella, lo que cambió exitosamente el valor del campo ahora final. ¿Estoy haciendo algo mal con los cambios de mi código de bytes, o la aplicación final solo la hace el compilador de Java?
Actualización: (31 de julio) Aquí hay un código para ti. Las partes principales son
- un simple POJO con un
private int x
private final int y
yprivate final int y
, - MakeFieldsFinalClassAdapter, que hace que todos los campos que visite sean definitivos a menos que ya lo sea,
- y el AddSetYMethodVisitor, que hace que el método setX () del POJO también establezca y en el mismo valor que establece x en.
En otras palabras, comenzamos con una clase con un campo final (x) y un campo no final (y). Hacemos x final. Hacemos setX () set y además de configurar x. Corremos. Tanto x como y se configuran sin errores. El código está en github . Puedes clonarlo con:
git clone git://github.com/zzantozz/testbed.git tmp
cd tmp/asm-playground
Dos cosas a tener en cuenta: la razón por la que hice esta pregunta en primer lugar: tanto un campo que hice como final y un campo que ya era definitivo se pueden establecer con lo que creo que son las instrucciones normales de bytecode.
Otra actualización: (1 de agosto) Probado con 1.6.0_26-b03 y 1.7.0-b147 con los mismos resultados. Es decir, la JVM felizmente modifica los campos finales en tiempo de ejecución.
Actualización final (?): (19 de septiembre) Estoy eliminando la fuente completa de esta publicación porque fue bastante larga, pero aún está disponible en github (ver más arriba).
Creo que he demostrado de manera concluyente que JDK7 JVM infringe la especificación . (Vea el extracto en la respuesta de Stephen .) Después de usar ASM para modificar el código de bytes como se describió anteriormente, lo escribí en un archivo de clase. Usando el excelente JD-GUI , este archivo de clase se descompila al siguiente código:
package rds.asm;
import java.io.PrintStream;
public class TestPojo
{
private final int x;
private final int y;
public TestPojo(int x)
{
this.x = x;
this.y = 1;
}
public int getX() {
return this.x;
}
public void setX(int x) {
System.out.println("Inside setX()");
this.x = x; this.y = x;
}
public String toString()
{
return "TestPojo{x=" +
this.x +
", y=" + this.y +
''}'';
}
public static void main(String[] args) {
TestPojo pojo = new TestPojo(10);
System.out.println(pojo);
pojo.setX(42);
System.out.println(pojo);
}
}
Un breve vistazo a eso debería decirle que la clase nunca se compilará debido a la reasignación de un campo final, y aún así, ejecutar esa clase en un plano simple de vainilla JDK 6 o 7 tiene este aspecto:
$ java rds.asm.TestPojo
TestPojo{x=10, y=1}
Inside setX()
TestPojo{x=42, y=42}
- ¿Alguien más tiene una entrada antes de informar un error en esto?
- ¿Alguien puede confirmar si esto debería ser un error en JDK 6 o solo en 7?
¿Es "final" final en tiempo de ejecución?
No en el sentido que quieres decir.
AFAIK, la semántica del modificador final
solo es impuesta por el compilador de bytecode.
No hay códigos de bytes especiales para inicializar los campos final
, y el verificador de códigos de bytes (aparentemente) tampoco verifica las asignaciones "ilegales".
Sin embargo, el compilador JIT podría tratar el modificador final
como una sugerencia de que las cosas no necesitan ser replanteadas. Por lo tanto, si sus códigos de bytes modifican una variable marcada como final
, es probable que cause un comportamiento impredecible. (Y lo mismo puede suceder si usa la reflexión para modificar una variable final
. La especificación lo dice claramente ...)
Y, por supuesto, puedes modificar un campo final
usando la reflexión.
ACTUALIZAR
Eché un vistazo a la especificación de Java 7 JVM, y en parte contradice lo que dije anteriormente. Específicamente, la descripción del código de operación PutField dice:
"Vincular excepciones ... De lo contrario, si el campo es final, debe declararse en la clase actual y la instrucción debe aparecer en un método de inicialización de instancia (
<init>
) de la clase actual. De lo contrario, se lanza unIllegalAccessError
. " .
Entonces, aunque podría (en teoría) asignar un campo final
varias veces en el constructor del objeto, el verificador de bytecode debería evitar cualquier intento de cargar un método que contenga un bytecode que se asigne a un final
. Lo cual ... cuando piensas en los entornos de seguridad de Java ... es algo bueno.
Puede sobrescribir los campos finales en tiempo de ejecución utilizando la reflexión. Gson hace esto todo el tiempo mientras vincula JSON a objetos Java.
Si el campo es final, todavía puede haber una situación cuando se le asigna. Por ejemplo en constructor. Esta lógica es impuesta por el compilador como se indica en este article . La propia JVM no aplicaría tales reglas, ya que el precio de rendimiento sería demasiado alto y el verificador de código de byte puede no ser capaz de determinar fácilmente si el campo se asigna solo una vez.
Así que hacer que el campo sea final
través de ASM, probablemente no tiene mucho sentido