Clausole di prova finale definite e dimostrate

Benvenuti a un'altra puntata di Under The Hood . Questa colonna offre agli sviluppatori Java un assaggio dei misteriosi meccanismi che scattano e ronzano sotto i loro programmi Java in esecuzione. L'articolo di questo mese continua la discussione sul set di istruzioni bytecode della Java virtual machine (JVM). Il suo obiettivo è il modo in cui la JVM gestisce le finallyclausole e i bytecode rilevanti per queste clausole.

Infine: qualcosa per cui rallegrarsi

Poiché la Java virtual machine esegue i bytecode che rappresentano un programma Java, può uscire da un blocco di codice - le istruzioni tra due parentesi graffe corrispondenti - in uno dei diversi modi. Per uno, la JVM potrebbe semplicemente essere eseguita oltre la parentesi graffa di chiusura del blocco di codice. Oppure potrebbe incontrare un'istruzione break, continue o return che lo fa saltare fuori dal blocco di codice da qualche parte nel mezzo del blocco. Infine, potrebbe essere generata un'eccezione che induce la JVM a saltare a una clausola catch corrispondente o, se non esiste una clausola catch corrispondente, a terminare il thread. Con questi potenziali punti di uscita esistenti all'interno di un singolo blocco di codice, è auspicabile disporre di un modo semplice per esprimere che qualcosa è accaduto indipendentemente da come si esce da un blocco di codice. In Java, un tale desiderio è espresso con atry-finally clausola.

Per utilizzare una try-finallyclausola:

  • racchiudere in un tryblocco il codice che ha più punti di uscita e

  • mettere in un finallyblocco il codice che deve accadere indipendentemente da come trysi esce dal blocco.

Per esempio:

prova {// blocco di codice con più punti di uscita} infine {// blocco di codice che viene sempre eseguito quando si esce dal blocco try, // indipendentemente da come si esce dal blocco try} 

Se hai delle catchclausole associate al tryblocco, devi inserire la finallyclausola dopo tutte le catchclausole, come in:

prova {// Blocco di codice con più punti di uscita} catch (Cold e) {System.out.println ("Caught cold!"); } catch (APopFly e) {System.out.println ("Caught a pop fly!"); } catch (SomeonesEye e) {System.out.println ("Catturato lo sguardo di qualcuno!"); } infine {// Blocco di codice che viene sempre eseguito quando si esce dal blocco try, // indipendentemente da come si esce dal blocco try. System.out.println ("È qualcosa per cui tifare?"); }

Se durante l'esecuzione del codice all'interno di un tryblocco, viene generata un'eccezione gestita da una catchclausola associata al tryblocco, la finallyclausola verrà eseguita dopo la catchclausola. Ad esempio, se Coldviene generata un'eccezione durante l'esecuzione delle istruzioni (non mostrate) nel tryblocco precedente, il testo seguente verrà scritto nell'output standard:

Preso freddo! È qualcosa per cui rallegrarsi?

Clausole di prova finale nei bytecode

Nei bytecode, le finallyclausole agiscono come subroutine in miniatura all'interno di un metodo. Ad ogni punto di uscita all'interno di un tryblocco e delle catchclausole associate , finallyviene chiamata la subroutine in miniatura che corrisponde alla clausola. Dopo il finallycompletamento della clausola, purché si completi eseguendo oltre l'ultima istruzione nella finallyclausola, non lanciando un'eccezione o eseguendo un return, continue o break, la subroutine in miniatura stessa ritorna. L'esecuzione prosegue appena oltre il punto in cui è stata chiamata la subroutine in miniatura, quindi tryè possibile uscire dal blocco nel modo appropriato.

Il codice operativo che fa passare la JVM a una subroutine in miniatura è l' istruzione jsr . L' istruzione jsr accetta un operando a due byte, l'offset dalla posizione dell'istruzione jsr dove inizia la subroutine in miniatura. Una seconda variante dell'istruzione jsr è jsr_w , che svolge la stessa funzione di jsr ma accetta un operando wide (quattro byte). Quando la JVM incontra un'istruzione jsr o jsr_w , inserisce un indirizzo di ritorno nello stack, quindi continua l'esecuzione all'inizio della subroutine in miniatura. L'indirizzo di ritorno è l'offset del bytecode immediatamente successivo a jsr ojsr_w e i suoi operandi.

Dopo il completamento di una subroutine in miniatura, richiama l' istruzione ret , che ritorna dalla subroutine. L' istruzione ret accetta un operando, un indice nelle variabili locali in cui è memorizzato l'indirizzo di ritorno. Gli opcode che trattano le finallyclausole sono riassunti nella seguente tabella:

Infine clausole
Codice operativo Operando (i) Descrizione
jsr branchbyte1, branchbyte2 spinge l'indirizzo di ritorno, si dirama in offset
jsr_w branchbyte1, branchbyte2, branchbyte3, branchbyte4 spinge l'indirizzo di ritorno, si dirama a un ampio offset
ret indice ritorna all'indirizzo memorizzato nell'indice della variabile locale

Non confondere una subroutine in miniatura con un metodo Java. I metodi Java utilizzano un diverso insieme di istruzioni. Istruzioni come invokevirtual o invokenonvirtual provocano il richiamo di un metodo Java e istruzioni come return , areturn o ireturn determinano la restituzione di un metodo Java. L' istruzione jsr non causa il richiamo di un metodo Java. Invece, provoca un salto a un codice operativo diverso all'interno dello stesso metodo. Allo stesso modo, l' istruzione ret non ritorna da un metodo; piuttosto, ritorna all'opcode nello stesso metodo che segue immediatamente l' istruzione jsr chiamante e i suoi operandi. I bytecode che implementano un filefinallyle clausole sono chiamate subroutine in miniatura perché agiscono come una piccola subroutine all'interno del flusso bytecode di un singolo metodo.

Potresti pensare che l' istruzione ret dovrebbe estrarre l'indirizzo di ritorno dallo stack, perché è lì che è stato spinto dall'istruzione jsr . Ma non è così. Invece, all'inizio di ogni subroutine, l'indirizzo di ritorno viene estratto dalla parte superiore dello stack e memorizzato in una variabile locale, la stessa variabile locale da cui successivamente l'istruzione ret lo ottiene. Questo modo asimmetrico di lavorare con l'indirizzo di ritorno è necessario perché finalmente clausole (e quindi, subroutine in miniatura) si possono generare eccezioni o includere return, breako continuedichiarazioni. A causa di questa possibilità, l'indirizzo di ritorno aggiuntivo che è stato inserito nello stack da jsristruzione deve essere rimossa dalla pila subito, quindi non sarà ancora lì se le finallyuscite clausola con break, continue, returno eccezione generata. Pertanto, l'indirizzo del mittente viene memorizzato in una variabile locale all'inizio della finallysubroutine in miniatura di qualsiasi clausola.

A titolo illustrativo, si consideri il codice seguente, che include una finallyclausola che termina con un'istruzione break. Il risultato di questo codice è che, indipendentemente dal parametro bVal passato al metodo surpriseTheProgrammer(), il metodo restituisce false:

static boolean surpriseTheProgrammer (boolean bVal) {while (bVal) {try {return true; } finalmente {pausa; }} return false; }

L'esempio sopra mostra perché l'indirizzo di ritorno deve essere memorizzato in una variabile locale all'inizio della finallyclausola. Poiché la finallyclausola termina con un'interruzione, non esegue mai l' istruzione ret . Di conseguenza, la JVM non torna mai a completare l' return trueistruzione " ". Invece, va avanti con il breake scende oltre la parentesi graffa di chiusura dell'istruzione while. L'affermazione successiva è " return false," che è esattamente ciò che fa la JVM.

Il comportamento mostrato da una finallyclausola che esce con a breakè mostrato anche da finallyclausole che escono con un returno continue, o generando un'eccezione. Se una finallyclausola esce per uno qualsiasi di questi motivi, l' istruzione ret alla fine della finallyclausola non viene mai eseguita. Poiché non è garantito che l'istruzione ret venga eseguita, non è possibile fare affidamento su di essa per rimuovere l'indirizzo di ritorno dallo stack. Pertanto, l'indirizzo del mittente viene memorizzato in una variabile locale all'inizio della finallysubroutine in miniatura della clausola.

Per un esempio completo, si consideri il seguente metodo, che contiene un tryblocco con due punti di uscita. In questo esempio, entrambi i punti di uscita sono returndichiarazioni:

static int giveMeThatOldFashionedBoolean (boolean bVal) {try {if (bVal) {return 1; } return 0; } finalmente {System.out.println ("Got old fashioned."); }}

Il metodo sopra viene compilato con i seguenti bytecode:

// La sequenza del bytecode per il blocco try: 0 iload_0 // Push variabile locale 0 (arg passato come divisore) 1 ifeq 11 // Push variabile locale 1 (arg passato come dividendo) 4 iconst_1 // Push int 1 5 istore_3 // Apri un int (l'1), memorizza nella variabile locale 3 6 jsr 24 // Salta alla mini-subroutine per la clausola finalmente 9 iload_3 // Spingi la variabile locale 3 (l'1) 10 ireturn // Restituisci int sopra stack (the 1) 11 iconst_0 // Push int 0 12 istore_3 // Pop an int (the 0), store in local variable 3 13 jsr 24 // Jump to the mini-subroutine for the latest clause 16 iload_3 // Push local variabile 3 (lo 0) 17 ireturn // Restituisce int in cima allo stack (lo 0) // La sequenza del bytecode per una clausola catch che cattura qualsiasi tipo di eccezione // lanciata dall'interno del blocco try. 18 astore_1 // Visualizza il riferimento all'eccezione generata,memorizza // nella variabile locale 1 19 jsr 24 // Salta alla mini-subroutine per la clausola infine 22 aload_1 // Sposta il riferimento (all'eccezione generata) dalla // variabile locale 1 23 avanti // Ripeti la stessa eccezione / / La subroutine in miniatura che implementa il blocco finalmente. 24 astore_2 // Inserisci l'indirizzo di ritorno, memorizzalo nella variabile locale 2 25 getstatic # 8 // Ottieni un riferimento a java.lang.System.out 28 ldc # 1 // Esegui push dal pool di costanti 30 invokevirtual # 7 // Invoca System.out.println () 33 ret 2 // Torna all'indirizzo di ritorno memorizzato nella variabile locale 2memorizzalo nella variabile locale 2 25 getstatic # 8 // Ottieni un riferimento a java.lang.System.out 28 ldc # 1 // Spingi dal pool di costanti 30 invokevirtual # 7 // Richiama System.out.println () 33 ret 2 // Torna all'indirizzo di ritorno memorizzato nella variabile locale 2memorizzalo nella variabile locale 2 25 getstatic # 8 // Ottieni un riferimento a java.lang.System.out 28 ldc # 1 // Spingi dal pool di costanti 30 invokevirtual # 7 // Richiama System.out.println () 33 ret 2 // Torna all'indirizzo di ritorno memorizzato nella variabile locale 2

I bytecode per il tryblocco includono due istruzioni jsr . Un'altra istruzione jsr è contenuta nella catchclausola. La catchclausola viene aggiunta dal compilatore perché se viene generata un'eccezione durante l'esecuzione del tryblocco, il blocco finalmente deve essere comunque eseguito. Pertanto, la catchclausola richiama semplicemente la subroutine in miniatura che rappresenta la finallyclausola, quindi genera di nuovo la stessa eccezione. La tabella delle eccezioni per il giveMeThatOldFashionedBoolean()metodo, mostrata di seguito, indica che qualsiasi eccezione generata tra e inclusi gli indirizzi 0 e 17 (tutti i bytecode che implementano il tryblocco) sono gestite dalla catchclausola che inizia all'indirizzo 18.

Tabella delle eccezioni: da al tipo di destinazione 0 18 18 qualsiasi 

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:

class Clown {static int hopAround () {int i = 0; while (vero) {prova {prova {i = 1; } finalmente {// la prima clausola finalmente i = 2; } i = 3; return i; // questo non si completa mai, a causa della clausola continue} infine {// la seconda clausola finalmente if (i == 3) {continue; // questo continue sovrascrive l'istruzione return}}}}}

Di seguito sono riportati i bytecode generati da javacper il hopAround()metodo: