關於 Java 中 finally 語句塊的深度辨析

青色刀客發表於2013-08-01

可不能小看這個簡單的 finally,看似簡單的問題背後,卻隱藏了無數的玄機。接下來我就帶您一步一步的揭開這個 finally 的神祕面紗。

問題分析

首先來問大家一個問題:finally 語句塊一定會執行嗎?

很多人都認為 finally 語句塊是肯定要執行的,其中也包括一些很有經驗的 Java 程式設計師。可惜並不像大多人所認為的那樣,對於這個問題,答案當然是否定的,我們先來看下面這個例子。


清單 1.
				
 public class Test { 
 public static void main(String[] args) { 
 System.out.println("return value of test(): " + test()); 
	 } 

 public static int test() { 
 int i = 1; 
		
 // 		 if(i == 1) 
 // 			 return 0; 
 System.out.println("the previous statement of try block"); 
 i = i / 0; 
		
 try { 
    System.out.println("try block"); 
      return i; 
     }finally { 
     System.out.println("finally block"); 
		 } 
	 } 
 } 

清單 1 的執行結果如下:

 the previous statement of try block 
 Exception in thread "main" java.lang.ArithmeticException: / by zero 
 at com.bj.charlie.Test.test(Test.java:15) 
 at com.bj.charlie.Test.main(Test.java:6) 

另外,如果去掉上例中被註釋的兩條語句前的註釋符,執行結果則是:

 return value of test(): 0 

在以上兩種情況下,finally 語句塊都沒有執行,說明什麼問題呢?只有與 finally 相對應的 try 語句塊得到執行的情況下,finally 語句塊才會執行。以上兩種情況,都是在 try 語句塊之前返回(return)或者丟擲異常,所以 try 對應的 finally 語句塊沒有執行。

那好,即使與 finally 相對應的 try 語句塊得到執行的情況下,finally 語句塊一定會執行嗎?不好意思,這次可能又讓大家失望了,答案仍然是否定的。請看下面這個例子(清單 2)。


清單 2.
				
 public class Test { 
 public static void main(String[] args) { 
 System.out.println("return value of test(): " + test()); 
	 } 

 public static int test() { 
 int i = 1; 

 try { 
 System.out.println("try block"); 
 System.exit(0); 
 return i; 
 }finally { 
 System.out.println("finally block"); 
		 } 
	 } 
 } 

清單 2 的執行結果如下:

 try block 

finally 語句塊還是沒有執行,為什麼呢?因為我們在 try 語句塊中執行了 System.exit (0) 語句,終止了 Java 虛擬機器的執行。那有人說了,在一般的 Java 應用中基本上是不會呼叫這個 System.exit(0) 方法的。OK !沒有問題,我們不呼叫 System.exit(0) 這個方法,那麼 finally 語句塊就一定會執行嗎?

再一次讓大家失望了,答案還是否定的。當一個執行緒在執行 try 語句塊或者 catch 語句塊時被打斷(interrupted)或者被終止(killed),與其相對應的 finally 語句塊可能不會執行。還有更極端的情況,就是線上程執行 try 語句塊或者 catch 語句塊時,突然當機或者斷電,finally 語句塊肯定不會執行了。可能有人認為當機、斷電這些理由有些強詞奪理,沒有關係,我們只是為了說明這個問題。

finally 語句剖析

說了這麼多,還是讓我們拿出些有說服力的證據吧!還有什麼證據比官方的文件更具說服力呢?讓我們來看看官方網站上的《The Java Tutorials》中是怎樣來描述 finally 語句塊的吧!

以下位於 **** 之間的內容原封不動的摘自於《 The Java Tutorials 》文件。

*******************************************************************************

The finally Block

The finally block always executes when the try block exits. This ensures that the finally block is executed even if an unexpected exception occurs. But finally is useful for more than just exception handling — it allows the programmer to avoid having cleanup code accidentally bypassed by a return,continue, or break. Putting cleanup code in a finally block is always a good practice, even when no exceptions are anticipated.

Note:  If the JVM exits while the try or catch code is being executed, then the finally block may not execute. Likewise, if the thread executing the try or catch code is interrupted or killed, the finally block may not execute even though the application as a whole continues.

*******************************************************************************

請仔細閱讀並認真體會一下以上兩段英文,當你真正的理解了這兩段英文的確切含義,你就可以非常自信的來回答“finally 語句塊是否一定會執行?”這樣的問題。看來,大多時候,並不是 Java 語言本身有多麼高深,而是我們忽略了對基礎知識的深入理解。

接下來,我們看一下 finally 語句塊是怎樣執行的。在排除了以上 finally 語句塊不執行的情況後,finally 語句塊就得保證要執行,既然 finally 語句塊一定要執行,那麼它和 try 語句塊與 catch 語句塊的執行順序又是怎樣的呢?還有,如果 try 語句塊中有 return 語句,那麼 finally 語句塊是在 return 之前執行,還是在 return 之後執行呢?帶著這樣一些問題,我們還是以具體的案例來講解。

關於 try、catch、finally 的執行順序問題,我們還是來看看權威的論述吧!以下 **** 之間的內容摘自 Java 語言規範第四版(《 The Java™ Programming Language, Fourth Edition 》)中對於 try,catch,和 finally 的描述。

*******************************************************************************

12.4. Try, catch, and finally

You catch exceptions by enclosing code in Try blocks. The basic syntax for a Try block is:

try {

statements

} catch (exception_type1 identifier1) {

statements

} catch (exception_type2 identifier2) {

statements

...

} finally {

statements

}

where either at least one catch clause, or the finally clause, must be present. The body of the try statement is executed until either an exception is thrown or the body finishes successfully. If an exception is thrown, each catch clause is examined in turn, from first to last, to see whether the type of the exception object is assignable to the type declared in the catch. When an assignable catch clause is found, its block is executed with its identifier set to reference the exception object. No other catch clause will be executed. Any number of catch clauses, including zero, can be associated with a particular TRy as long as each clause catches a different type of exception. If no appropriate catch is found, the exception percolates out of the try statement into any outer try that might have a catch clause to handle it.

If a finally clause is present with a try, its code is executed after all other processing in the try is complete. This happens no matter how completion was achieved, whether normally, through an exception, or through a control flow statement such as return or break.

*******************************************************************************

上面這段文字的大體意思是說,不管 try 語句塊正常結束還是異常結束,finally 語句塊是保證要執行的。如果 try 語句塊正常結束,那麼在 try 語句塊中的語句都執行完之後,再執行 finally 語句塊。如果 try 中有控制轉移語句(return、break、continue)呢?那 finally 語句塊是在控制轉移語句之前執行,還是之後執行呢?似乎從上面的描述中我們還看不出任何端倪,不要著急,後面的講解中我們會分析這個問題。如果 try 語句塊異常結束,應該先去相應的 catch 塊做異常處理,然後執行 finally 語句塊。同樣的問題,如果 catch 語句塊中包含控制轉移語句呢? finally 語句塊是在這些控制轉移語句之前,還是之後執行呢?我們也會在後續討論中提到。

其實,關於 try,catch,finally 的執行流程遠非這麼簡單,有興趣的讀者可以參考 Java 語言規範第三版(《 The Java™ Language Specification, Third Edition 》)中對於 Execution of try-catch-finally 的描述,非常複雜的一個流程。限於篇幅的原因,本文不做摘錄,請感興趣的讀者自行閱讀。

finally 語句示例說明

下面,我們先來看一個簡單的例子(清單 3)。


清單 3.
				
 public class Test { 
 public static void main(String[] args) {  
 try {  
 System.out.println("try block");  

 return ;  
 } finally {  
 System.out.println("finally block");  
		 }  
	 }  
 } 

清單 3 的執行結果為:

 try block 
 finally block 

清單 3 說明 finally 語句塊在 try 語句塊中的 return 語句之前執行。我們再來看另一個例子(清單 4)。


清單 4.
				
 public class Test { 
 public static void main(String[] args) {  
 System.out.println("reture value of test() : " + test()); 
	 } 
	
 public static int test(){ 
 int i = 1; 
		
 try {  
 System.out.println("try block");  
			 i = 1 / 0; 
 return 1;  
 }catch (Exception e){ 
 System.out.println("exception block"); 
 return 2; 
 }finally {  
 System.out.println("finally block");  
		 } 
	 } 
 } 

清單 4 的執行結果為:

 try block 
 exception block 
 finally block 
 reture value of test() : 2 

清單 4 說明了 finally 語句塊在 catch 語句塊中的 return 語句之前執行。

從上面的清單 3 和清單 4,我們可以看出,其實 finally 語句塊是在 try 或者 catch 中的 return 語句之前執行的。更加一般的說法是,finally 語句塊應該是在控制轉移語句之前執行,控制轉移語句除了 return 外,還有 break 和 continue。另外,throw 語句也屬於控制轉移語句。雖然 return、throw、break 和 continue 都是控制轉移語句,但是它們之間是有區別的。其中 return 和 throw 把程式控制權轉交給它們的呼叫者(invoker),而 break 和 continue 的控制權是在當前方法內轉移。請大家先記住它們的區別,在後續的分析中我們還會談到。

還是得來點有說服力的證據,下面這段摘自 Java 語言規範第四版(《 The Java™ Programming Language, Fourth Edition 》),請讀者自己體會一下其含義。

*******************************************************************************

Afinallyclause can also be used to clean up forbreak,continue, andreturn, which is one reason you will sometimes see atryclause with nocatchclauses. When any control transfer statement is executed, all relevantfinallyclauses are executed. There is no way to leave atryblock without executing itsfinallyclause.

*******************************************************************************

好了,看到這裡,是不是有人認為自己已經掌握了 finally 的用法了?先別忙著下結論,我們再來看兩個例子 – 清單 5 和清單 6。


清單 5.
				
 public class Test { 
 public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
	 } 

 public static int getValue() { 
        try { 
                 return 0; 
        } finally { 
                 return 1; 
	        } 
	 } 
 } 

清單 5 的執行結果:

 return value of getValue(): 1 


清單 6.
				
 public class Test { 
 public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
	 } 

 public static int getValue() { 
        int i = 1; 
        try { 
                 return i; 
        } finally { 
                 i++; 
        } 
	 } 
 } 

清單 6 的執行結果:

 return value of getValue(): 1 

利用我們上面分析得出的結論:finally 語句塊是在 try 或者 catch 中的 return 語句之前執行的。 由此,可以輕鬆的理解清單 5 的執行結果是 1。因為 finally 中的 return 1;語句要在 try 中的 return 0;語句之前執行,那麼 finally 中的 return 1;語句執行後,把程式的控制權轉交給了它的呼叫者 main()函式,並且返回值為 1。那為什麼清單 6 的返回值不是 2,而是 1 呢?按照清單 5 的分析邏輯,finally 中的 i++;語句應該在 try 中的 return i;之前執行啊? i 的初始值為 1,那麼執行 i++;之後為 2,再執行 return i;那不就應該是 2 嗎?怎麼變成 1 了呢?

關於 Java 虛擬機器是如何編譯 finally 語句塊的問題,有興趣的讀者可以參考《 The JavaTM Virtual Machine Specification, Second Edition 》中 7.13 節 Compiling finally。那裡詳細介紹了 Java 虛擬機器是如何編譯 finally 語句塊。實際上,Java 虛擬機器會把 finally 語句塊作為 subroutine(對於這個 subroutine 不知該如何翻譯為好,乾脆就不翻譯了,免得產生歧義和誤解。)直接插入到 try 語句塊或者 catch 語句塊的控制轉移語句之前。但是,還有另外一個不可忽視的因素,那就是在執行 subroutine(也就是 finally 語句塊)之前,try 或者 catch 語句塊會保留其返回值到本地變數表(Local Variable Table)中。待 subroutine 執行完畢之後,再恢復保留的返回值到運算元棧中,然後通過 return 或者 throw 語句將其返回給該方法的呼叫者(invoker)。請注意,前文中我們曾經提到過 return、throw 和 break、continue 的區別,對於這條規則(保留返回值),只適用於 return 和 throw 語句,不適用於 break 和 continue 語句,因為它們根本就沒有返回值。

是不是不太好理解,那我們就用具體的例子來做形象的說明吧!

為了能夠解釋清單 6 的執行結果,我們來分析一下清單 6 的位元組碼(byte-code):

 Compiled from "Test.java"
 public class Test extends java.lang.Object{ 
 public Test(); 
  Code: 
   0: 	 aload_0 
   1:invokespecial#1; //Method java/lang/Object."<init>":()V 
   4: 	 return 

  LineNumberTable: 
   line 1: 0 

 public static void main(java.lang.String[]); 
  Code: 
   0: 	 getstatic 	 #2; //Field java/lang/System.out:Ljava/io/PrintStream; 
   3: 	 new 	 #3; //class java/lang/StringBuilder 
   6: 	 dup 
   7: 	 invokespecial 	 #4; //Method java/lang/StringBuilder."<init>":()V 
   10: 	 ldc 	 #5; //String return value of getValue(): 
   12: 	 invokevirtual 	 
   #6; //Method java/lang/StringBuilder.append:(
       Ljava/lang/String;)Ljava/lang/StringBuilder; 
   15: 	 invokestatic 	 #7; //Method getValue:()I 
   18: 	 invokevirtual 	 
   #8; //Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 
   21: 	 invokevirtual 	 
   #9; //Method java/lang/StringBuilder.toString:()Ljava/lang/String; 
   24: 	 invokevirtual 	 #10; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 
   27: 	 return 

 public static int getValue(); 
  Code: 
   0: 	 iconst_1 
   1: 	 istore_0 
   2: 	 iload_0 
   3: 	 istore_1 
   4: 	 iinc 	 0, 1 
   7: 	 iload_1 
   8: 	 ireturn 
   9: 	 astore_2 
   10: 	 iinc 	 0, 1 
   13: 	 aload_2 
   14: 	 athrow 
  Exception table: 
   from   to  target type 
     2     4     9   any 
     9    10     9   any 
 } 

對於 Test()構造方法與 main()方法,在這裡,我們不做過多解釋。讓我們來分析一下 getValue()方法的執行。在這之前,先讓我把 getValue()中用到的虛擬機器指令解釋一下,以便讀者能夠正確的理解該函式的執行。

1.	iconst_ 
Description: Push the int constant  (-1, 0, 1, 2, 3, 4 or 5) onto the operand stack.
Forms: iconst_m1 = 2 (0x2)  iconst_0 = 3 (0x3)  iconst_1 = 4 (0x4)  
iconst_2 = 5 (0x5) iconst_3 = 6 (0x6)  iconst_4 = 7 (0x7)  iconst_5 = 8 (0x8)

2.	istore_ 
Description: Store int into local variable. The  must be an index into the 
local variable array of the current frame. 
Forms: istore_0 = 59 (0x3b)  istore_1 = 60 (0x3c)  istore_2 = 61 (0x3d)  
istore_3 = 62 (0x3e)

3.	iload_ 
Description: Load int from local variable. The  must be an index into the 
local variable array of the current frame. 
Forms: iload_0 = 26 (0x1a)  iload_1 = 27 (0x1b)  iload_2 = 28 (0x1c)  iload_3 = 29 (0x1d)

4.	iinc index, const 
Description: Increment local variable by constant. The index is an unsigned byte that 
must be an index into the local variable array of the current frame. The const is an 
immediate signed byte. The local variable at index must contain an int. The value 
const is first sign-extended to an int, and then the local variable at index is 
incremented by that amount.
Forms:  iinc = 132 (0x84)

Format:
iinc 	
index 	
const 	

5.	ireturn 
Description: Return int from method.
Forms:  ireturn = 172 (0xac)

6.	astore_ 
Description: Store reference into local variable. The  must be an index into the 
local variable array of the current frame.
Forms: astore_0 = 75 (0x4b) astore_1 = 76 (0x4c) astore_2 =77 (0x4d) astore_3 =78 (0x4e)

7.	aload_ 
Description: Load reference from local variable. The  must be an index into the 
local variable array of the current frame.
Forms: aload_0 = 42 (0x2a) aload_1 = 43 (0x2b) aload_2 = 44 (0x2c) aload_3 = 45 (0x2d)

8.	athrow 
Description: Throw exception or error.
Forms: athrow = 191 (0xbf)



有了以上的 Java 虛擬機器指令,我們來分析一下其執行順序:分為正常執行(沒有 exception)和異常執行(有 exception)兩種情況。我們先來看一下正常執行的情況,如圖 1 所示:


圖 1. getValue()函式正常執行的情況
圖 1. getValue()函式正常執行的情況 

由上圖,我們可以清晰的看出,在 finally 語句塊(iinc 0, 1)執行之前,getValue()方法儲存了其返回值(1)到本地表量表中 1 的位置,完成這個任務的指令是 istore_1;然後執行 finally 語句塊(iinc 0, 1),finally 語句塊把位於 0 這個位置的本地變數表中的值加 1,變成 2;待 finally 語句塊執行完畢之後,把本地表量表中 1 的位置上值恢復到運算元棧(iload_1),最後執行 ireturn 指令把當前運算元棧中的值(1)返回給其呼叫者(main)。這就是為什麼清單 6 的執行結果是 1,而不是 2 的原因。

再讓我們來看看異常執行的情況。是不是有人會問,你的清單 6 中都沒有 catch 語句,哪來的異常處理呢?我覺得這是一個好問題,其實,即使沒有 catch 語句,Java 編譯器編譯出的位元組碼中還是有預設的異常處理的,別忘了,除了需要捕獲的異常,還可能有不需捕獲的異常(如:RunTimeException 和 Error)。

從 getValue()方法的位元組碼中,我們可以看到它的異常處理表(exception table), 如下:

Exception table:

from to target type

2 4 9 any

它的意思是說:如果從 2 到 4 這段指令出現異常,則由從 9 開始的指令來處理。


圖 2. getValue()函式異常執行的情況
圖 2. getValue()函式異常執行的情況 

先說明一點,上圖中的 exception 其實應該是 exception 物件的引用,為了方便說明,我直接把它寫成 exception 了。

由上圖(圖 2)可知,當從 2 到 4 這段指令出現異常時,將會產生一個 exception 物件,並且把它壓入當前運算元棧的棧頂。接下來是 astore_2 這條指令,它負責把 exception 物件儲存到本地變數表中 2 的位置,然後執行 finally 語句塊,待 finally 語句塊執行完畢後,再由 aload_2 這條指令把預先儲存的 exception 物件恢復到運算元棧中,最後由 athrow 指令將其返回給該方法的呼叫者(main)。

通過以上的分析,大家應該已經清楚 try-catch-finally 語句塊的執行流程了吧!

為了更具說服力,我們還是來引經據典吧!大家可以不相信我,難道還不相信“高司令”(Gosling)嗎?下面這段仍然摘自 Java 語言規範第四版 《 The Java™ Programming Language, Fourth Edition》,請讀者自己體會吧!

*******************************************************************************

a finally clause is always entered with a reason. That reason may be that the try code finished normally, that it executed a control flow statement such as return, or that an exception was thrown in code executed in the Try block. The reason is remembered when the finally clause exits by falling out the bottom. However, if the finally block creates its own reason to leave by executing a control flow statement (such as break or return) or by throwing an exception, that reason supersedes the original one, and the original reason is forgotten. For example, consider the following code:
try {
// … do something … 
return 1;
} finally {
return 2;
}
When the Try block executes its return, the finally block is entered with the “reason” of returning the value 1. However, inside the finally block the value 2 is returned, so the initial intention is forgotten. In fact, if any of the other code in the try block had thrown an exception, the result would still be to return 2. If the finally block did not return a value but simply fell out the bottom, the “return the value 1 ″ reason would be remembered and carried out.

*******************************************************************************

好了,有了以上的知識,讓我們再來看以下 3 個例子。


清單 7.
				
 public class Test { 
 public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
	 } 

 @SuppressWarnings("finally") 
 public static int getValue() { 
        int i = 1; 
        try { 
                 i = 4; 
        } finally { 
                 i++; 
                 return i; 
        } 
	 } 
 } 

清單 7 的執行結果:

 return value of getValue(): 5 


清單 8.
				
 public class Test { 
 public static void main(String[] args) { 
        System.out.println("return value of getValue(): " + getValue()); 
	 } 

 public static int getValue() { 
        int i = 1; 
        try { 
                 i = 4; 
        } finally { 
                 i++; 
        } 
        
        return i; 
	 } 
 } 

清單 8 的執行結果:

 return value of getValue(): 5 

清單 7 和清單 8 應該還比較簡單吧!利用我們上面講解的知識,很容易分析出其結果。讓我們再來看一個稍微複雜一點的例子 – 清單 9。我建議大家最好先不要看執行結果,運用學過的知識來分析一下,看是否能推斷出正確的結果。


清單 9.
				
 public class Test { 
 public static void main(String[] args) {  
 System.out.println(test());  
	 }  

 public static String test() {  
 try {  
 System.out.println("try block");  
 return test1();  
 } finally {  
 System.out.println("finally block");  
		 }  
	 }  
 public static String test1() {  
 System.out.println("return statement");  
 return "after return";  
	 }  
 } 

清單 9 的結果:

 try block 
 return statement 
 finally block 
 after return 

你分析對了嗎?其實這個案例也不算很難,return test1();這條語句等同於 :

 String tmp = test1(); 
 return tmp; 

這樣,就應該清楚為什麼是上面所示的執行結果了吧!

好了,就寫到這吧!希望大家看完這篇文章能夠有所收穫!

總結

沒想到吧!一個小小的、看似簡單的 finally 語句塊背後居然隱藏了這麼多玄機。看來,我們平時還是應該認真的閱讀 Java 相關的基礎文件,比如:Java 語言規範、Java 虛擬機器規範等,很多棘手的問題都可以從中得到答案。只有真正的吃透了基礎知識,才能達到運用自如的境界!


http://www.ibm.com/developerworks/cn/java/j-lo-finally/


相關文章