java - not - ¿Podría reasignarse una variable final en catch, incluso si la asignación es la última operación en try?
try catch java netbeans (12)
Al navegar por javadoc, parece que no se pudo lanzar una subclase de Exception
justo después de que se me asignó. Desde la perspectiva teórica de JLS, parece que se puede generar un Error
justo después de que se me asigna (por ejemplo, VirtualMachineError
).
Parece que no hay requisitos de JLS para que el compilador determine si podría establecerse previamente cuando se alcanza el bloque catch, al distinguir si está capturando Exception
o Error
/ Throwable
, lo que implica que es una debilidad del modelo JLS.
¿Por qué no probar lo siguiente? (ha compilado y probado)
(Entero Tipo de Contenedor + finalmente + "Elvis" operador para probar si nulo):
import myUtils.ExpressionUtil;
....
Integer i0 = null;
final int i;
try { i0 = calculateIndex(); } // method may return int - autoboxed to Integer!
catch (Exception e) {}
finally { i = nvl(i0,1); }
package myUtils;
class ExpressionUtil {
// Custom-made, because shorthand Elvis operator left out of Java 7
Integer nvl(Integer i0, Integer i1) { return (i0 == null) ? i1 : i0;}
}
Estoy bastante convencido de que aquí
final int i;
try { i = calculateIndex(); }
catch (Exception e) { i = 1; }
no puedo haber sido asignado si el control alcanza el bloque de captura. Sin embargo, el compilador de Java no está de acuerdo y afirma que the final local variable i may already have been assigned
.
¿Todavía me falta algo de sutileza, o es solo una debilidad del modelo utilizado por la Especificación del lenguaje Java para identificar posibles reasignaciones? Mi principal preocupación son cosas como Thread.stop()
, que pueden dar como resultado que se Thread.stop()
una excepción "de la nada", pero todavía no veo cómo podría lanzarse después de la tarea, que aparentemente es la última acción. dentro del try-block.
La expresión anterior, si se permite, simplificaría muchos de mis métodos. Tenga en cuenta que este caso de uso cuenta con soporte de primera clase en idiomas, como Scala, que emplea consistentemente la mónada Maybe :
final int i = calculateIndex().getOrElse(1);
Creo que este caso de uso sirve como una motivación bastante buena para permitir ese caso especial en el que definitivamente no estoy asignado dentro del bloque de captura.
ACTUALIZAR
Después de pensarlo, estoy aún más seguro de que esto es solo una debilidad del modelo JLS: si declaro el axioma "en el ejemplo presentado, definitivamente no estoy asignado cuando el control alcanza el bloque catch", no entrará en conflicto con ningún otro axioma o teorema. El compilador no permitirá ninguna lectura de i
antes de que se asigne en el bloque catch, por lo que no se puede observar el hecho de si se i
ha asignado o no.
Caza JLS:
Es un error en tiempo de compilación si se asigna una variable final a menos que definitivamente no esté asignada (§16) inmediatamente antes de la asignación.
Quoth capítulo 16:
V definitivamente no está asignado antes de un bloque catch si se cumplen todas las condiciones siguientes:
V definitivamente no está asignado después del bloque try.
V definitivamente no está asignado antes de cada declaración de devolución que pertenece al bloque try.
V definitivamente no está asignado después de e en cada instrucción de la forma throw e que pertenece al bloque try.
V definitivamente no está asignado después de cada declaración afirmativa que ocurre en el bloque try.
V definitivamente no está asignado antes de cada sentencia break que pertenece al bloque try y cuyo objetivo de ruptura contiene (o es) la sentencia try.
V definitivamente no está asignado antes de cada instrucción continua que pertenece al bloque try y cuyo objetivo continuo contiene la instrucción try.
Bold es mío. Después del bloqueo de try
, no está claro si i
asignado.
Además en el ejemplo
final int i;
try {
i = foo();
bar();
}
catch(Exception e) { // e might come from bar
i = 1;
}
El texto en negrita es la única condición que impide que la asignación incorrecta errónea i=1
sea ilegal. Por lo tanto, esto es suficiente para demostrar que una condición más fina de "definitivamente no asignado" es necesaria para permitir el código en su publicación original.
Si la especificación fue revisada para reemplazar esta condición con
V definitivamente no está asignado después del bloque try, si el bloque catch captura una excepción no verificada.
V definitivamente no está asignado antes de la última declaración capaz de arrojar una excepción de un tipo capturado por el bloque catch, si el bloque catch capta una excepción no verificada.
Entonces creo que tu código sería legal. (Lo mejor de mi análisis ad-hoc)
Envié un JSR para esto, que espero que se ignore, pero tenía curiosidad por ver cómo se manejan. Técnicamente, el número de fax es un campo obligatorio, espero que no cause demasiado daño si ingresé allí + 1-000-000-000.
Creo que hay una situación en la que este modelo actúa como salvavidas. Considere el siguiente código:
final Integer i;
try
{
i = new Integer(10);----->(1)
}catch(Exception ex)
{
i = new Integer(20);
}
Ahora considere la línea (1). La mayoría de los compiladores JIT crea objetos en la siguiente secuencia (código de psuedo):
mem = allocate(); //Allocate memory
ctorInteger(instance);//Invoke constructor for Singleton passing instance.
i = mem; //Make instance i non-null
Pero, algunos compiladores JIT no escribe fuera de orden . Y los pasos anteriores se reordenan de la siguiente manera:
mem = allocate(); //Allocate memory
i = mem; //Make instance i non-null
ctorInteger(instance); //Invoke constructor for Singleton passing instance.
Ahora supongamos que el JIT
realiza out of order writes
mientras crea el objeto en la línea (1). Y supongamos que se lanza una excepción al ejecutar el constructor. En ese caso, el bloque catch
tendrá i
que not null
es not null
. Si JVM no sigue este modal, entonces, en este caso, la variable final puede asignarse dos veces.
Creo que la JVM es, por desgracia, correcta. Si bien es intuitivamente correcto al mirar el código, tiene sentido en el contexto de mirar el IL. Creé un método run () simple que casi imita tu caso (comentarios simplificados aquí):
0: aload_0
1: invokevirtual #5; // calculateIndex
4: istore_1
5: goto 17
// here''s the catch block
17: // is after the catch
Entonces, aunque no se puede escribir fácilmente código para probar esto, porque no se compilará, la invocación del método, el almacenamiento del valor y el salto a después de la captura son tres operaciones separadas. Podría (por muy improbable que sea) que ocurra una excepción (Thread.interrupt () parece ser el mejor ejemplo) entre el paso 4 y el paso 5. Esto daría como resultado la entrada al bloque catch después de que se configuró.
No estoy seguro de que pueda hacer que eso ocurra intencionalmente con una tonelada de hilos e interrupciones (y el compilador no le permitirá escribir ese código de todos modos), pero es teóricamente posible que yo pueda establecerlo, y podría ingresar el bloque de manejo de excepciones, incluso con este código simple.
Este es un resumen de los argumentos más sólidos a favor de la tesis de que las reglas actuales para la asignación definitiva no pueden relajarse sin romper la consistencia (A), seguidas de mis contraargumentos (B):
A : en el nivel de bytecode, la escritura en la variable no es la última instrucción dentro del try-block: por ejemplo, la última instrucción será típicamente un salto de salto sobre el código de manejo de excepciones;
B : pero si las reglas indican que definitivamente no estoy asignado dentro del catch-block, su valor puede no ser observado. Un valor inobservable es tan bueno como ningún valor;
R : incluso si el compilador declara
i
como definitivamente no asignado , una herramienta de depuración aún podría ver el valor;B : de hecho, una herramienta de depuración siempre podría acceder a una variable local no inicializada, que en una implementación típica tendrá un valor arbitrario. No existe una diferencia esencial entre una variable no inicializada y una variable cuya inicialización se completó abruptamente después de que se produjo la escritura real. Independientemente del caso especial considerado aquí, la herramienta siempre debe usar metadatos adicionales para saber para cada variable local el rango de instrucciones donde esa variable está definitivamente asignada y solo permite que se observe su valor mientras la ejecución se encuentra dentro del rango.
Conclusión final:
La especificación podría recibir sistemáticamente reglas más precisas que permitirían compilar mi ejemplo publicado.
Me enfrenté EXACTAMENTE con el mismo problema, Mario, y leí esta discusión muy interesante. Acabo de resolver mi problema con eso:
private final int i;
public Byte(String hex) {
int calc;
try {
calc = Integer.parseInt(hex, 16);
} catch (NumberFormatException e) {
calc = 0;
}
finally {
i = calc;
}
}
@ Joeg, debo admitir que me gustó mucho su publicación sobre el diseño, especialmente esa frase: calculateIndx () calcula a veces el índice , pero ¿podríamos decir lo mismo sobre parseInt ()? ¿No es eso también el papel de calculateIndex () arrojar y por lo tanto no calcular el índice cuando no es posible, y luego hacer que devuelva un valor incorrecto (1 es arbitrario en su refactorización) es realmente malo.
@Marko, no entendí tu respuesta a Joeg sobre la línea AFTER 4 y ANTES de la línea 5 ... Todavía no soy lo suficientemente fuerte en el mundo java (25y de c ++ pero solo 1 en java ...), pero En este caso, el compilador tiene razón: podría inicializar dos veces en el caso de Joeg.
[Todo lo que digo es una opinión muy muy humilde]
No tan limpio (y sospecho que ya lo estás haciendo). Pero esto solo agrega 1 línea adicional.
final int i;
int temp;
try { temp = calculateIndex(); }
catch (IOException e) { temp = 1; }
i = temp;
Pero i
pueden asignar dos veces
int i;
try {
i = calculateIndex(); // suppose func returns true
System.out.println("i=" + i);
throw new IOException();
} catch (IOException e) {
i = 1;
System.out.println("i=" + i);
}
salida
i=0
i=1
y significa que no puede ser final
Según las especificaciones de la búsqueda de JLS realizada por "djechlin", las especificaciones indican cuándo la variable está definitivamente desasignada. Así que la especificación dice que en esos escenarios es seguro permitir la asignación. Puede haber situaciones distintas a la mencionada en las especificaciones, en cuyo caso la variable aún puede ser desasignada y dependerá del compilador para tomar esa decisión inteligente si puede detectar y permitir una tarea.
Spec no menciona de ninguna manera en el escenario especificado por usted, que el compilador debe marcar un error. Por lo tanto, depende de la implementación del compilador de la especificación si es lo suficientemente inteligente como para detectar dichos escenarios.
Referencia: sección de Asignación Definitiva de Especificación de Lenguaje Java "16.2.15 Declaraciones de prueba"
Tiene razón en que si la asignación es la última operación en el bloque de prueba, sabemos que al ingresar al bloque catch la variable no se habrá asignado. Sin embargo, formalizar la noción de "última operación" añadiría una complejidad significativa a la especificación. Considerar:
try {
foo = bar();
if (foo) {
i = 4;
} else {
i = 7;
}
}
¿Esa característica sería útil? No lo creo, porque una variable final debe asignarse exactamente una vez, como máximo una vez. En su caso, la variable no se asignaría si se produce un Error
. Es posible que no le importe eso si la variable se sale del alcance de todos modos, pero ese no es siempre el caso (podría haber otro bloque catch atrapando el Error
, en la misma o en una declaración de intento circundante). Por ejemplo, considere:
final int i;
try {
try {
i = foo();
} catch (Exception e) {
bar();
i = 1;
}
} catch (Throwable t) {
i = 0;
}
Eso es correcto, pero no lo sería si la llamada a bar () se produce después de asignar i (como en la cláusula finally), o utilizamos una declaración try-with-resources con un recurso cuyo método close arroja una excepción.
La contabilidad de eso agregaría aún más complejidad a la especificación.
Finalmente, hay un trabajo simple:
final int i = calculateIndex();
y
int calculateIndex() {
try {
// calculate it
return calculatedIndex;
} catch (Exception e) {
return 0;
}
}
eso hace que sea obvio que tengo asignado.
En resumen, creo que agregar esta característica agregaría una complejidad significativa a la especificación para obtener un pequeño beneficio.
EDICIÓN DE RESPUESTA BASADA EN PREGUNTAS DESDE OP
Esto es realmente en respuesta al comentario:
Todo lo que ha hecho está escrito un ejemplo claro de un argumento de hombre de paja: está presentando de forma indirecta la suposición tácita de que siempre debe haber un único valor predeterminado, válido para todos los sitios de llamadas.
Creo que estamos abordando toda la cuestión desde extremos opuestos. Parece que lo estás mirando de abajo hacia arriba, literalmente desde el bytecode y yendo hasta Java. Si esto no es cierto, lo está viendo desde el cumplimiento del "código" hasta la especificación.
Al abordar esto desde la dirección opuesta, desde el "diseño" hacia abajo, veo problemas. Creo que fue M. Fowler quien recopiló varios "malos olores" en el libro: "Refactorización: mejorar el diseño del código existente". Aquí (y probablemente muchos, muchos otros lugares) se describe la refactorización del "Método de extracción".
Por lo tanto, si imagino una versión inventada de tu código sin el método ''calculateIndex'', podría tener algo como esto:
public void someMethod() {
final int i;
try {
int intermediateVal = 35;
intermediateVal += 56;
i = intermediateVal*3;
} catch (Exception e) {
// would like to be able to set i = 1 here;
}
}
Ahora, lo anterior PODRÍA haberse refactorizado como se publicó originalmente con un método ''calculateIndex''. Sin embargo, si la Refactorización del ''Método de Extracción'' definida por Fowler se aplica por completo, entonces se obtiene esto [nota: dejar caer la ''e'' es intencional para diferenciarlo de su método].
public void someMethod() {
final int i = calculateIndx();
}
private int calculateIndx() {
try {
int intermediateVal = 35;
intermediateVal += 56;
return intermediateVal*3;
} catch (Exception e) {
return 1; // or other default values or other way of setting
}
}
Entonces, desde la perspectiva del ''diseño'', el problema es el código que tienes. Su método ''calculateIndex'' NO calcula el índice. Solo a veces . El resto del tiempo, el manejador de excepciones realiza el cálculo.
Además, esta refactorización es mucho más complaciente con los cambios. Por ejemplo, si tiene que cambiar lo que asumí que era el valor predeterminado de ''1'' a ''2'', no es gran cosa. Sin embargo, como se señala en la respuesta OP citada, no se puede suponer que solo hay un valor predeterminado. Si la lógica para configurar esto crece solo un poco compleja, aún podría residir fácilmente en el manejador de excepción encapsulado. Sin embargo, en algún momento, también puede necesitar ser refactorizado en su propio método. Ambos casos aún permiten que el método encapsulado realice su función y realmente calcule el índice.
En resumen, cuando llego aquí y veo lo que creo que es el código correcto, no hay problema de compilación para la discusión. (Estoy seguro de que no estarás de acuerdo: está bien, solo quiero ser más claro sobre mi punto de vista). En cuanto a las advertencias de compilación que surgen por código incorrecto, esas me ayudan a darme cuenta, en primer lugar, de que algo anda mal. En este caso, esa refactorización es necesaria.
1 final int i;
2 try { i = calculateIndex(); }
3 catch (Exception e) {
4 i = 1;
5 }
OP ya comenta que en la línea 4 es posible que ya haya sido asignado. Por ejemplo, a través de Thread.stop (), que es una excepción asíncrona, consulte http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5
Ahora configure un punto de interrupción en la línea 4 y puede observar el estado de la variable i antes de asignar 1. Por lo tanto, aflojar el comportamiento observado iría en contra de la interfaz Java ™ Virtual Machine Tool