Definidas y demostradas las cláusulas de prueba final

Bienvenido a otra entrega de Under The Hood . Esta columna ofrece a los desarrolladores de Java una idea de los misteriosos mecanismos que hacen clic y zumban debajo de sus programas Java en ejecución. El artículo de este mes continúa la discusión del conjunto de instrucciones de código de bytes de la máquina virtual Java (JVM). Su enfoque es la forma en que la JVM maneja las finallycláusulas y los códigos de bytes que son relevantes para estas cláusulas.

Finalmente: algo para alegrar

A medida que la máquina virtual Java ejecuta los códigos de bytes que representan un programa Java, puede salir de un bloque de código (las declaraciones entre dos llaves coincidentes) de una de varias formas. Por un lado, la JVM simplemente podría ejecutarse más allá de la llave de cierre del bloque de código. O bien, podría encontrar una declaración de interrupción, continuación o devolución que haga que salte del bloque de código desde algún lugar en el medio del bloque. Finalmente, se podría lanzar una excepción que haga que la JVM salte a una cláusula catch coincidente o, si no hay una cláusula catch coincidente, que termine el hilo. Dado que estos puntos de salida potenciales existen dentro de un solo bloque de código, es deseable tener una manera fácil de expresar que algo sucedió sin importar cómo se salga de un bloque de código. En Java, tal deseo se expresa con untry-finally cláusula.

Para usar una try-finallycláusula:

  • encierre en un trybloque el código que tiene múltiples puntos de salida, y

  • ponga en un finallybloque el código que debe suceder sin importar cómo tryse salga del bloque.

Por ejemplo:

try {// Bloque de código con múltiples puntos de salida} finalmente {// Bloque de código que siempre se ejecuta cuando se sale del bloque try, // no importa cómo se sale del bloque try} 

Si tiene alguna catchcláusula asociada con el trybloque, debe poner la finallycláusula después de todas las catchcláusulas, como en:

try {// Bloque de código con múltiples puntos de salida} catch (Cold e) {System.out.println ("¡Atrapado en frío!"); } catch (APopFly e) {System.out.println ("¡Atrapé una mosca pop!"); } catch (SomeonesEye e) {System.out.println ("¡Le llamó la atención a alguien!"); } finalmente {// Bloque de código que siempre se ejecuta cuando se sale del bloque try, // sin importar cómo se sale del bloque try. System.out.println ("¿Es eso algo por lo que alegrarse?"); }

Si durante la ejecución del código dentro de un trybloque, se lanza una excepción que es manejada por una catchcláusula asociada con el trybloque, la finallycláusula se ejecutará después de la catchcláusula. Por ejemplo, si Coldse lanza una excepción durante la ejecución de las declaraciones (no mostradas) en el trybloque anterior, el siguiente texto se escribiría en la salida estándar:

¡Me resfrie! ¿Es eso algo por lo que alegrarse?

Cláusulas de prueba finalmente en códigos de bytes

En los códigos de bytes, las finallycláusulas actúan como subrutinas en miniatura dentro de un método. En cada punto de salida dentro de un trybloque y sus catchcláusulas asociadas , finallyse llama a la subrutina en miniatura que corresponde a la cláusula. Una vez que la finallycláusula se completa, siempre que se complete ejecutando más allá de la última declaración de la finallycláusula, no lanzando una excepción o ejecutando un retorno, continuar o romper, la subrutina en miniatura en sí regresa. La ejecución continúa justo después del punto donde se llamó a la subrutina en miniatura en primer lugar, por lo tryque se puede salir del bloque de la manera adecuada.

El código de operación que hace que la JVM salte a una subrutina en miniatura es la instrucción jsr . La instrucción jsr toma un operando de dos bytes, el desplazamiento desde la ubicación de la instrucción jsr donde comienza la subrutina en miniatura. Una segunda variante de la instrucción jsr es jsr_w , que realiza la misma función que jsr pero toma un operando ancho (cuatro bytes). Cuando la JVM encuentra una instrucción jsr o jsr_w , inserta una dirección de retorno en la pila y luego continúa la ejecución al comienzo de la subrutina en miniatura. La dirección de retorno es el desplazamiento del código de bytes que sigue inmediatamente al jsr oinstrucción jsr_w y sus operandos.

Después de que se completa una subrutina en miniatura, invoca la instrucción ret , que regresa de la subrutina. La instrucción ret toma un operando, un índice en las variables locales donde se almacena la dirección de retorno. Los códigos de operación que tratan con finallycláusulas se resumen en la siguiente tabla:

Finalmente cláusulas
Código de operación Operando (s) Descripción
jsr branchbyte1, branchbyte2 empuja la dirección de retorno, se ramifica para compensar
jsr_w branchbyte1, branchbyte2, branchbyte3, branchbyte4 empuja la dirección de retorno, se ramifica a un desplazamiento amplio
ret índice vuelve a la dirección almacenada en el índice de la variable local

No confunda una subrutina en miniatura con un método Java. Los métodos de Java utilizan un conjunto de instrucciones diferente. Las instrucciones como invokevirtual o invokenonvirtual hacen que se invoque un método Java, y las instrucciones como return , areturn o ireturn hacen que regrese un método Java. La instrucción jsr no hace que se invoque un método Java. En cambio, provoca un salto a un código de operación diferente dentro del mismo método. Asimismo, la instrucción ret no regresa de un método; más bien, regresa al código de operación en el mismo método que sigue inmediatamente a la instrucción jsr que llama y sus operandos. Los códigos de bytes que implementan unfinallyLas cláusulas se denominan subrutina en miniatura porque actúan como una pequeña subrutina dentro del flujo de código de bytes de un solo método.

Podría pensar que la instrucción ret debería sacar la dirección de retorno de la pila, porque ahí es donde fue empujada por la instrucción jsr . Pero no es así. En cambio, al comienzo de cada subrutina, la dirección de retorno se extrae de la parte superior de la pila y se almacena en una variable local, la misma variable local de la que luego la obtiene la instrucción ret . Es necesaria esta manera asimétrica de trabajo con la dirección de retorno, porque finalmente las cláusulas (y por lo tanto, subrutinas en miniatura) sí pueden lanzar excepciones o incluir return, breako continuedeclaraciones. Debido a esta posibilidad, la dirección de retorno adicional que fue empujada a la pila por jsrla instrucción debe ser retirado de la pila de inmediato, por lo que no todavía estará allí si las finallysalidas con una cláusula break, continue, return, o excepción lanzada. Por lo tanto, la dirección de retorno se almacena en una variable local al comienzo de finallyla subrutina en miniatura de cualquier cláusula.

Como ilustración, considere el siguiente código, que incluye una finallycláusula que sale con una declaración de interrupción. El resultado de este código es que, independientemente del parámetro bVal pasado al método surpriseTheProgrammer(), el método devuelve false:

static boolean surpriseTheProgrammer (boolean bVal) {while (bVal) {try {return true; } finalmente {romper; } } falso retorno; }

El ejemplo anterior muestra por qué la dirección de retorno debe almacenarse en una variable local al comienzo de la finallycláusula. Debido a que la finallycláusula sale con una interrupción, nunca ejecuta la instrucción ret . Como resultado, la JVM nunca regresa para terminar la return truedeclaración " ". En su lugar, sigue adelante con breaky pasa por debajo de la llave de cierre de la whiledeclaración. La siguiente declaración es " return false," que es precisamente lo que hace la JVM.

El comportamiento que muestra una finallycláusula que sale con a breaktambién se muestra mediante finallycláusulas que salen con a returno continue, o lanzando una excepción. Si una finallycláusula sale por cualquiera de estos motivos, la instrucción ret al final de la finallycláusula nunca se ejecuta. Debido a que no se garantiza que la instrucción ret se ejecute, no se puede confiar en ella para eliminar la dirección de retorno de la pila. Por lo tanto, la dirección de retorno se almacena en una variable local al comienzo de la finallysubrutina en miniatura de la cláusula.

Para obtener un ejemplo completo, considere el siguiente método, que contiene un trybloque con dos puntos de salida. En este ejemplo, ambos puntos de salida son returndeclaraciones:

static int giveMeThatOldFashionedBoolean (bVal booleano) {try {if (bVal) {return 1; } return 0; } finalmente {System.out.println ("Se pasó de moda"); }}

El método anterior se compila con los siguientes códigos de bytes:

// La secuencia de código de bytes para el bloque try: 0 iload_0 // Insertar la variable local 0 (arg se pasa como divisor) 1 ifeq 11 // Insertar la variable local 1 (arg se pasa como dividendo) 4 iconst_1 // Insertar int 1 5 istore_3 // Haga estallar un int (el 1), almacénelo en la variable local 3 6 jsr 24 // Salte a la mini-subrutina para la cláusula finalmente 9 iload_3 // Inserte la variable local 3 (el 1) 10 ireturn // Regrese int encima stack (the 1) 11 iconst_0 // Push int 0 12 istore_3 // Pop an int (the 0), store into local variable 3 13 jsr 24 // Salta a la mini-subrutina para la cláusula finalmente 16 iload_3 // Push local variable 3 (el 0) 17 ireturn // Devuelve int encima de la pila (el 0) // La secuencia de código de bytes para una cláusula catch que detecta cualquier tipo de excepción // lanzada desde dentro del bloque try. 18 astore_1 // Abre la referencia a la excepción lanzada,almacenar // en la variable local 1 19 jsr 24 // Saltar a la mini-subrutina para la cláusula finalmente 22 aload_1 // Insertar la referencia (a la excepción lanzada) desde // la variable local 1 23 en la fila // Relanzar la misma excepción / / La subrutina en miniatura que implementa el bloque finalmente. 24 astore_2 // Pop la dirección de retorno, guárdela en la variable local 2 25 getstatic # 8 // Obtenga una referencia a java.lang.System.out 28 ldc # 1 // Empuje desde el grupo constante 30 invokevirtual # 7 // Invoque System.out.println () 33 ret 2 // Regresar a la dirección de retorno almacenada en la variable local 2almacenarlo en la variable local 2 25 getstatic # 8 // Obtener una referencia a java.lang.System.out 28 ldc # 1 // Insertar desde el grupo constante 30 invokevirtual # 7 // Invocar System.out.println () 33 ret 2 // Volver a la dirección de retorno almacenada en la variable local 2almacenarlo en la variable local 2 25 getstatic # 8 // Obtener una referencia a java.lang.System.out 28 ldc # 1 // Insertar desde el grupo constante 30 invokevirtual # 7 // Invocar System.out.println () 33 ret 2 // Volver a la dirección de retorno almacenada en la variable local 2

The bytecodes for the try block include two jsr instructions. Another jsr instruction is contained in the catch clause. The catch clause is added by the compiler because if an exception is thrown during the execution of the try block, the finally block must still be executed. Therefore, the catch clause merely invokes the miniature subroutine that represents the finally clause, then throws the same exception again. The exception table for the giveMeThatOldFashionedBoolean() method, shown below, indicates that any exception thrown between and including addresses 0 and 17 (all the bytecodes that implement the try block) are handled by the catch clause that starts at address 18.

Exception table: from to target type 0 18 18 any 

The bytecodes of the finally clause begin by popping the return address off the stack and storing it into local variable two. At the end of the finally clause, the ret instruction takes its return address from the proper place, local variable two.

HopAround: A Java virtual machine simulation

The applet below demonstrates a Java virtual machine executing a sequence of bytecodes. The bytecode sequence in the simulation was generated by the javac compiler for the hopAround() method of the class shown below:

clase Payaso {static int hopAround () {int i = 0; while (verdadero) {prueba {prueba {i = 1; } finalmente {// la primera cláusula final i = 2; } i = 3; volver i; // esto nunca se completa, debido a la continuación} finalmente {// la segunda cláusula finalmente si (i == 3) {continuar; // esta continuación anula la declaración de retorno}}}}}

Los códigos de bytes generados por javacpara el hopAround()método se muestran a continuación: