JAVA面試題解惑系列(四)——final、finally和finalize的區別

作者:臧圩人(zangweiren)
網址:http://zangweiren.javaeye.com

>>>轉載請註明出處! <<<

final、finally和finalize的區別是什麼?

這是一道再經典不過的面試題了,我們在各個公司的面試題中幾乎都能看到它的身影。final、finally和finalize雖然長得像孿生三兄弟一樣,但是它們的含義和用法卻是大相徑庭。這一次我們就一起來回顧一下這方面的知識。

我們首先來說說final。它可以用於以下四個地方:

  1. 定義變量,包括靜態的和非靜態的。
  2. 定義方法的參數。
  3. 定義方法。
  4. 定義類。


我們依次來回顧一下每種情況下final的作用。首先來看第一種情況,如果final修飾的是一個基本類型,就表示這個變量被賦予的值是不可變的,即它是 個常量;如果final修飾的是一個對象,就表示這個變量被賦予的引用是不可變的,這裏需要提醒大家注意的是,不可改變的只是這個變量所保存的引用,並不 是這個引用所指向的對象。在第二種情況下,final的含義與第一種情況相同。實際上對於前兩種情況,有一種更貼切的表述final的含義的描述,那就 是,如果一個變量或方法參數被final修飾,就表示它只能被賦值一次,但是JAVA虛擬機爲變量設定的默認值不記作一次賦值。

被final修飾的變量必須被初始化。初始化的方式有以下幾種:

  1. 在定義的時候初始化。
  2. final變量可以在初始化塊中初始化,不可以在靜態初始化塊中初始化。
  3. 靜態final變量可以在靜態初始化塊中初始化,不可以在初始化塊中初始化。
  4. final變量還可以在類的構造器中初始化,但是靜態final變量不可以。


通過下面的代碼可以驗證以上的觀點:

Java代碼 複製代碼
  1. public   class  FinalTest {   
  2.      // 在定義時初始化   
  3.      public   final   int  A =  10 ;   
  4.   
  5.      public   final   int  B;   
  6.      // 在初始化塊中初始化   
  7.     {   
  8.         B =  20 ;   
  9.     }   
  10.   
  11.      // 非靜態final變量不能在靜態初始化塊中初始化   
  12.      // public final int C;   
  13.      // static {   
  14.      // C = 30;   
  15.      // }   
  16.   
  17.      // 靜態常量,在定義時初始化   
  18.      public   static   final   int  STATIC_D =  40 ;   
  19.   
  20.      public   static   final   int  STATIC_E;   
  21.      // 靜態常量,在靜態初始化塊中初始化   
  22.      static  {   
  23.         STATIC_E =  50 ;   
  24.     }   
  25.   
  26.      // 靜態變量不能在初始化塊中初始化   
  27.      // public static final int STATIC_F;   
  28.      // {   
  29.      // STATIC_F = 60;   
  30.      // }   
  31.   
  32.      public   final   int  G;   
  33.   
  34.      // 靜態final變量不可以在構造器中初始化   
  35.      // public static final int STATIC_H;   
  36.   
  37.      // 在構造器中初始化   
  38.      public  FinalTest() {   
  39.         G =  70 ;   
  40.          // 靜態final變量不可以在構造器中初始化   
  41.          // STATIC_H = 80;   
  42.   
  43.          // 給final的變量第二次賦值時,編譯會報錯   
  44.          // A = 99;   
  45.          // STATIC_D = 99;   
  46.     }   
  47.   
  48.      // final變量未被初始化,編譯時就會報錯   
  49.      // public final int I;   
  50.   
  51.      // 靜態final變量未被初始化,編譯時就會報錯   
  52.      // public static final int STATIC_J;   
  53. }  


我們運行上面的代碼之後出了可以發現final變量(常量)和靜態final變量(靜態常量)未被初始化時,編譯會報錯。

用final修飾的變量(常量)比非final的變量(普通變量)擁有更高的效率,因此我們在實際編程中應該儘可能多的用常量來代替普通變量,這也是一個很好的編程習慣。

 

當final用來定義一個方法時,會有什麼效果呢?正如大家所知,它表示這個方法不可以被子類重寫,但是它這不影響它被子類繼承。我們寫段代碼來驗證一下:

Java代碼 複製代碼
  1. class  ParentClass {   
  2.      public   final   void  TestFinal() {   
  3.         System.out.println( "父類--這是一個final方法" );   
  4.     }   
  5. }   
  6.   
  7. public   class  SubClass  extends  ParentClass {   
  8.      /**  
  9.      * 子類無法重寫(override)父類的final方法,否則編譯時會報錯  
  10.      */   
  11.      // public void TestFinal() {   
  12.      // System.out.println("子類--重寫final方法");   
  13.      // }   
  14.        
  15.      public   static   void  main(String[] args) {   
  16.         SubClass sc =  new  SubClass();   
  17.         sc.TestFinal();   
  18.     }   
  19. }  


這裏需要特殊說明的是,具有private訪問權限的方法也可以增加final修飾,但是由於子類無法繼承private方法,因此也無法重寫它。編譯器 在處理private方法時,是按照final方法來對待的,這樣可以提高該方法被調用時的效率。不過子類仍然可以定義同父類中的private方法具有 同樣結構的方法,但是這並不會產生重寫的效果,而且它們之間也不存在必然聯繫。

最後我們再來回顧一下final用於類的情況。這個大家應該也很熟悉了,因爲我們最常用的String類就是final的。由於final類不允許被繼 承,編譯器在處理時把它的所有方法都當作final的,因此final類比普通類擁有更高的效率。final的類的所有方法都不能被重寫,但這並不表示 final的類的屬性(變量)值也是不可改變的,要想做到final類的屬性值不可改變,必須給它增加final修飾,請看下面的例子:

Java代碼 複製代碼
  1. public   final   class  FinalTest {   
  2.   
  3.      int  i =  10 ;   
  4.   
  5.      public   static   void  main(String[] args) {   
  6.         FinalTest ft =  new  FinalTest();   
  7.         ft.i =  99 ;   
  8.         System.out.println(ft.i);   
  9.     }   
  10. }  


運行上面的代碼試試看,結果是99,而不是初始化時的10。

接下來我們一起回顧一下finally的用法。這個就比較簡單了,它只能用在try/catch語句中,並且附帶着一個語句塊,表示這段語句最終總是被執行。請看下面的代碼:

Java代碼 複製代碼
  1. public   final   class  FinallyTest {   
  2.      public   static   void  main(String[] args) {   
  3.          try  {   
  4.              throw   new  NullPointerException();   
  5.         }  catch  (NullPointerException e) {   
  6.             System.out.println( "程序拋出了異常" );   
  7.         }  finally  {   
  8.             System.out.println( "執行了finally語句塊" );   
  9.         }   
  10.     }   
  11. }  


運行結果說明了finally的作用:

  1. 程序拋出了異常
  2. 執行了finally語句塊


請大家注意,捕獲程序拋出的異常之後,既不加處理,也不繼續向上拋出異常,並不是良好的編程習慣,它掩蓋了程序執行中發生的錯誤,這裏只是方便演示,請不要學習。

那麼,有沒有一種情況使finally語句塊得不到執行呢?大家可能想到了return、continue、break這三個可以打亂代碼順序執行語句的規律。那我們就來試試看,這三個語句是否能影響finally語句塊的執行:

Java代碼 複製代碼
  1. public   final   class  FinallyTest {   
  2.   
  3.      // 測試return語句   
  4.      public  ReturnClass testReturn() {   
  5.          try  {   
  6.              return   new  ReturnClass();   
  7.         }  catch  (Exception e) {   
  8.             e.printStackTrace();   
  9.         }  finally  {   
  10.             System.out.println( "執行了finally語句" );   
  11.         }   
  12.          return   null ;   
  13.     }   
  14.   
  15.      // 測試continue語句   
  16.      public   void  testContinue() {   
  17.          for  ( int  i =  0 ; i <  3 ; i++) {   
  18.              try  {   
  19.                 System.out.println(i);   
  20.                  if  (i ==  1 ) {   
  21.                      continue ;   
  22.                 }   
  23.             }  catch  (Exception e) {   
  24.                 e.printStackTrace();   
  25.             }  finally  {   
  26.                 System.out.println( "執行了finally語句" );   
  27.             }   
  28.         }   
  29.     }   
  30.   
  31.      // 測試break語句   
  32.      public   void  testBreak() {   
  33.          for  ( int  i =  0 ; i <  3 ; i++) {   
  34.              try  {   
  35.                 System.out.println(i);   
  36.                  if  (i ==  1 ) {   
  37.                      break ;   
  38.                 }   
  39.             }  catch  (Exception e) {   
  40.                 e.printStackTrace();   
  41.             }  finally  {   
  42.                 System.out.println( "執行了finally語句" );   
  43.             }   
  44.         }   
  45.     }   
  46.   
  47.      public   static   void  main(String[] args) {   
  48.         FinallyTest ft =  new  FinallyTest();   
  49.          // 測試return語句   
  50.         ft.testReturn();   
  51.         System.out.println();   
  52.          // 測試continue語句   
  53.         ft.testContinue();   
  54.         System.out.println();   
  55.          // 測試break語句   
  56.         ft.testBreak();   
  57.     }   
  58. }   
  59.   
  60. class  ReturnClass {   
  61.      public  ReturnClass() {   
  62.         System.out.println( "執行了return語句" );   
  63.     }   
  64. }  


上面這段代碼的運行結果如下:

  1. 執行了return語句
  2. 執行了finally語句
  3.  
  4. 0
  5. 執行了finally語句
  6. 1
  7. 執行了finally語句
  8. 2
  9. 執行了finally語句
  10.  
  11. 0
  12. 執行了finally語句
  13. 1
  14. 執行了finally語句

很明顯,return、continue和break都沒能阻止finally語句塊的執行。從輸出的結果來看,return語 句似乎在finally語句塊之前執行了,事實真的如此嗎?我們來想想看,return語句的作用是什麼呢?是退出當前的方法,並將值或對象返回。如果 finally語句塊是在return語句之後執行的,那麼return語句被執行後就已經退出當前方法了,finally語句塊又如何能被執行呢?因 此,正確的執行順序應該是這樣的:編譯器在編譯return new ReturnClass();時,將它分成了兩個步驟,new ReturnClass()和return,前一個創建對象的語句是在finally語句塊之前被執行的,而後一個return語句是在finally語 句塊之後執行的,也就是說finally語句塊是在程序退出方法之前被執行的。同樣,finally語句塊是在循環被跳過(continue)和中斷 (break)之前被執行的。

最後,我們再來看看finalize,它是一個方法,屬於java.lang.Object類,它的定義如下:

Java代碼 複製代碼
  1. protected   void  finalize()  throws  Throwable { }  


衆所周知,finalize()方法是GC(garbage collector)運行機制的一部分,關於GC的知識我們將在後續的章節中來回顧。

在此我們只說說finalize()方法的作用是什麼呢?

finalize()方法是在GC清理它所從屬的對象時被調用的,如果執行它的過程中拋出了無法捕獲的異常(uncaught exception),GC將終止對改對象的清理,並且該異常會被忽略;直到下一次GC開始清理這個對象時,它的finalize()會被再次調用。

請看下面的示例:

Java代碼 複製代碼
  1. public   final   class  FinallyTest {   
  2.      // 重寫finalize()方法   
  3.      protected   void  finalize()  throws  Throwable {   
  4.         System.out.println( "執行了finalize()方法" );   
  5.     }   
  6.   
  7.      public   static   void  main(String[] args) {   
  8.         FinallyTest ft =  new  FinallyTest();   
  9.         ft =  null ;   
  10.         System.gc();   
  11.     }   
  12. }  


運行結果如下:

  • 執行了finalize()方法


程序調用了java.lang.System類的gc()方法,引起GC的執行,GC在清理ft對象時調用了它的finalize()方法,因此纔有了上面的輸出結果。調用System.gc()等同於調用下面這行代碼:

Java代碼 複製代碼
  1. Runtime.getRuntime().gc();  


調用它們的作用只是建議垃圾收集器(GC)啓動,清理無用的對象釋放內存空間,但是GC的啓動並不是一定的,這由JAVA虛擬機來決定。直到JAVA虛擬 機停止運行,有些對象的finalize()可能都沒有被運行過,那麼怎樣保證所有對象的這個方法在JAVA虛擬機停止運行之前一定被調用呢?答案是我們 可以調用System類的另一個方法:

Java代碼 複製代碼
  1. public   static   void  runFinalizersOnExit( boolean  value) {   
  2.      //other code   
  3. }  


給這個方法傳入true就可以保證對象的finalize()方法在JAVA虛擬機停止運行前一定被運行了,不過遺憾的是這個方法是不安全的,它會導致有用的對象finalize()被誤調用,因此已經不被贊成使用了。

由於finalize()屬於Object類,因此所有類都有這個方法,Object的任意子類都可以重寫(override)該方法,在其中釋放系統資源或者做其它的清理工作,如關閉輸入輸出流。

通過以上知識的回顧,我想大家對於final、finally、finalize的用法區別已經很清楚了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章