una subclases que programacion poo orientada objetos objeto miembros metodos metodo herencia ejemplos clases clase atributos atributo java static compilation final class-variables

subclases - que es un objeto en java



¿Por qué mi programa no muestra un error de tiempo de compilación cuando la variable de clase final no se inicializa? (4)

Para el siguiente código:

public class StaticFinal { private final static int i ; public StaticFinal() {} }

Obtengo el error de tiempo de compilación:

StaticFinal.java:7: variable i might not have been initialized {} ^ 1 error

Lo cual está de acuerdo con JLS8.3.1.2 , que dice que:

Es un error de tiempo de compilación si una variable de clase final en blanco (§4.12.4) no está definitivamente asignada (§16.8) por un inicializador estático (§8.7) de la clase en la que se declara.

Entonces, el error anterior se entiende completamente.
Pero ahora considera lo siguiente:

public class StaticFinal { private final static int i ; public StaticFinal()throws InstantiationException { throw new InstantiationException("Can''t instantiate"); // Don''t let the constructor to complete. } }

Aquí, el constructor nunca termina porque InstantiationException se lanza en medio del constructor. ¡Y este código compila bien!
¿Por qué es? ¿Por qué este código no muestra el error de tiempo de compilación sobre la no inicialización de final variable final i ?


EDITAR
Lo estoy compilando usando javac 1.6.0_25 en el símbolo del sistema (sin usar ningún IDE )


Como entiendo aquí, todos somos desarrolladores, así que creo que no encontraremos la respuesta real entre nosotros ... esto tiene algo que ver con el funcionamiento interno del compilador ... y creo que es un error, o al menos un comportamiento no deseado

Excluyendo Eclipse, que tiene algún tipo de compilador incremental (y por lo tanto es capaz de detectar inmediatamente el problema), la línea de comando javac realiza una compilación de un solo disparo. Ahora, el primer fragmento

public class StaticFinal { private final static int i ; }

que es básicamente lo mismo que tener un constructor vacío (como en el primer ejemplo), está arrojando el error en tiempo de compilación, y esto está bien porque está respetando las especificaciones.

En el segundo fragmento, creo que hay un error en el compilador; parece que el compilador toma algunas decisiones basadas en lo que está haciendo el constructor. Esto es más evidente si intenta compilar este,

public class StaticFinal { private final static int i ; public StaticFinal() { throw new RuntimeException("Can''t instantiate"); } }

Esto es más extraño que su ejemplo porque la excepción no verificada no está declarada en la firma del método y será (al menos esto es lo que pensé antes de leer esta publicación) descubierta solo en tiempo de ejecución.

Observando el comportamiento que podría decir (pero está mal según las especificaciones) eso.

Para variables finales estáticas, el compilador intenta ver si se inicializan explícitamente o se inicializan en un bloque de inicializador estático, pero, por alguna extraña razón, también busca algo en el constructor:

  • si se inicializan en el constructor, el compilador producirá un error (no se puede asignar allí un valor para una variable estática final)
  • si el constructor está vacío, el compilador producirá un error (si compila el primer ejemplo, el que tiene el constructor explícito de cero argumentos, el compilador se rompe indicando el corchete de cierre del constructor como la línea de error).
  • si no se puede crear una instancia de la clase porque el constructor no completa porque se lanza una excepción (esto no es cierto, por ejemplo, si escribe System.exit (1) en lugar de lanzar una excepción ... ¡no se compilará!), entonces el valor predeterminado será asignado a la variable estática (!)

Yo diría que es simplemente porque cuando agregas los Throws , básicamente estás manejando el error, por lo que el compilador responde "oh, bueno, probablemente él sepa lo que está haciendo entonces". Todavía da un error de tiempo de ejecución, después de todo.


Curiosamente, el código compilará si el campo está marcado como static y, en IntelliJ, se quejará (pero compilará) con el campo estático, y no dirá una palabra con el campo no estático.

Tiene razón en que JLS §8.1.3.2 tiene ciertas reglas con respecto a los campos finales [estáticos]. Sin embargo, hay algunas otras reglas alrededor de los campos finales que juegan un papel importante aquí, viniendo de la Especificación del lenguaje Java §4.12.4 - que especifica la semántica de compilación de un campo final .

Pero antes de que podamos entrar en esa bola de cera, necesitamos determinar qué sucede cuando vemos los throws , lo cual nos es dado por §14.18 , énfasis mío:

Una instrucción throw causa una excepción (§11) para ser lanzada. El resultado es una transferencia de control inmediata (§11.3) que puede salir de múltiples instrucciones y constructor múltiple, inicializador de instancia, inicializador estático y evaluaciones de inicializador de campo, e invocaciones de método hasta que se encuentra una sentencia try (§14.20) que capta el valor arrojado. Si no se encuentra dicha instrucción try, entonces la ejecución del hilo (§17) que ejecutó el lanzamiento finaliza (§11.3) después de la invocación del método uncaughtException para el grupo de hilos al que pertenece el hilo.

En términos simples: durante el tiempo de ejecución, si encontramos una instrucción throws , puede interrumpir la ejecución del constructor (formalmente, "se completa abruptamente"), haciendo que el objeto no se construya o construya en un estado incompleto. Esto podría ser un agujero de seguridad, dependiendo de la plataforma y la integridad parcial del constructor.

Lo que la JVM espera, dado por §4.5, es que un campo con el conjunto ACC_FINAL nunca tenga su valor establecido después de la construcción del objeto :

Declarado final; nunca asignado directamente a la construcción posterior del objeto (JLS §17.5).

Por lo tanto, estamos en un aprieto: esperamos un comportamiento de esto durante el tiempo de ejecución , pero no durante el tiempo de compilación . ¿Y por qué IntelliJ levanta un leve alboroto cuando tengo static en ese campo, pero no cuando no lo hago?

Primero, de vuelta a los throws : solo hay un error en tiempo de compilación con esa declaración si una de estas tres piezas no se cumple :

  • La expresión que se está lanzando está desmarcada o es nula,
  • try catch la excepción, y la está catch con el tipo correcto, o
  • La expresión arrojada es algo que realmente se puede arrojar, según §8.4.6 y §8.8.5.

Entonces, compilar un constructor con throws es legítimo. Simplemente sucede que, en el tiempo de ejecución, siempre se completará abruptamente.

Si una instrucción throw está contenida en una declaración de constructor, pero su valor no es capturado por alguna instrucción try que lo contenga, entonces la expresión de creación de instancia de clase que invoca al constructor se completará abruptamente debido a throw (§15.9.4).

Ahora, en ese campo final blanco. Hay una pieza curiosa para ellos: su tarea solo importa después del final del constructor, enfatiza la suya.

Una variable de instancia final en blanco debe asignarse definitivamente (§16.9) al final de cada constructor (§8.8) de la clase en la que se declara; de lo contrario, se produce un error en tiempo de compilación.

¿Qué pasa si nunca llegamos al final del constructor?

Primer programa: instanciación normal de un campo static final , descompilado:

// class version 51.0 (51) // access flags 0x21 public class com//sandbox/DecompileThis { // compiled from: DecompileThis.java // access flags 0x1A private final static I i = 10 // access flags 0x1 public <init>()V L0 LINENUMBER 7 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V L1 LINENUMBER 9 L1 RETURN // <- Pay close attention here. L2 LOCALVARIABLE this Lcom//sandbox/DecompileThis; L0 L2 0 MAXSTACK = 1 MAXLOCALS = 1 }

Observe que realmente llamamos a una instrucción RETURN después de llamar con éxito a nuestro <init> . Tiene sentido, y es perfectamente legal.

Segundo programa: lanza el constructor y el campo static final blanco, descompilado:

// class version 51.0 (51) // access flags 0x21 public class com//sandbox/DecompileThis { // compiled from: DecompileThis.java // access flags 0x1A private final static I i // access flags 0x1 public <init>()V throws java/lang/InstantiationException L0 LINENUMBER 7 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V L1 LINENUMBER 8 L1 NEW java/lang/InstantiationException DUP LDC "Nothin'' doin''." INVOKESPECIAL java/lang/InstantiationException.<init> (Ljava/lang/String;)V ATHROW // <-- Eeek, where''d my RETURN instruction go?! L2 LOCALVARIABLE this Lcom//sandbox/DecompileThis; L0 L2 0 MAXSTACK = 3 MAXLOCALS = 1 }

Las reglas de ATHROW indican que la referencia está reventada, y si hay un controlador de excepción por ahí, que contendrá la dirección de la instrucción sobre el manejo de la excepción. De lo contrario, se elimina de la pila.

Nunca volvemos explícitamente, lo que implica que nunca completamos la construcción del objeto. Por lo tanto, se puede considerar que el objeto está en un estado medio inestable, todo el tiempo obedeciendo las reglas de tiempo de compilación , es decir, todas las declaraciones son alcanzables .

En el caso de un campo estático, ya que no se considera una variable de instancia, sino una variable de clase, parece incorrecto que este tipo de invocación sea permisible. Puede valer la pena presentar un error en contra.

Pensando en ello, tiene sentido en contexto, ya que la siguiente declaración en Java es legal, y los cuerpos de los métodos son congruentes con los cuerpos constructores:

public boolean trueOrDie(int val) { if(val > 0) { return true; } else { throw new IllegalStateException("Non-natural number!?"); } }


Después de agregar un método principal para imprimir el código i . El código imprime el valor 0. Esto implica que el compilador java inicializa el i con el valor 0 automáticamente. Lo escribí en IntelliJ y tuve que deshabilitar la verificación del código para poder construir el código. De lo contrario, no me dejaría dar el mismo error que estaba recibiendo antes de lanzar la excepción.

Código JAVA: no inicializado

public class StaticFinal { private final static int i; public StaticFinal(){ throw new InstantiationError("Can''t instantiate!"); } public static void main(String args[]) { System.out.print(i); } }

Descompilado

Idéntico

Código JAVA: Inicializado

public class StaticFinal { private final static int i = 0; public StaticFinal(){ throw new InstantiationError("Can''t instantiate!"); } public static void main(String args[]) { System.out.print(StaticFinal.i); } }

Descompilado

public class StaticFinal { public StaticFinal() { throw new InstantiationError("Can''t instantiate!"); } public static void main(String args[]) { System.out.print(0); } private static final int i = 0; }

Después de descompilar el código, resulta que este no es el caso. Como los códigos descompilados y el original son idénticos. La única otra posibilidad es que la inicialización se realice a través de la Máquina Virtual de Java. Los últimos cambios que hice son una evidencia bastante buena de que es el caso.

Tengo que decirte bien por detectar esto.

Preguntas relacionadas: Aquí