看完這篇 final、finally 和 finalize 和麵試官扯皮就沒問題了

程式設計師cxuan發表於2020-11-02
我把自己以往的文章彙總成為了 Github ,歡迎各位大佬 star
https://github.com/crisxuan/bestJavaer
已提交此篇文章

final001

final 是 Java 中的關鍵字,它也是 Java 中很重要的一個關鍵字,final 修飾的類、方法、變數有不同的含義;finally 也是一個關鍵字,不過我們可以使用 finally 和其他關鍵字結合做一些組合操作; finalize 是一個不讓人待見的方法,它是物件祖宗 Object 中的一個方法,finalize 機制現在已經不推薦使用了。本篇文章,cxuan 就帶你從這三個關鍵字入手,帶你從用法、應用、原理的角度帶你深入淺出理解這三個關鍵字。

final、finally 和 finalize

我相信在座的各位都是資深程式設計師,final 這種基礎關鍵字就不用多說了。不過,還是要照顧一下小白讀者,畢竟我們都是從小白走過來的嘛。

final 修飾類、屬性和方法

final 可以用來修飾類,final 修飾的類不允許其他類繼承,也就是說,final 修飾的類是獨一無二的。如下所示

final002

我們首先定義了一個 FinalUsage 類,它使用 final 修飾,同時我們又定義了一個 FinalUsageExtend 類,它想要繼承(extend)FinalUsage,我們如上繼承後,編譯器不讓我們這麼玩兒,它提示我們 不能從 FinalUsage 類繼承,為什麼呢?不用管,這是 Java 的約定,有一些為什麼沒有必要,遵守就行。

final 可以用來修飾方法,final 修飾的方法不允許被重寫,我們先演示一下不用 final 關鍵字修飾的情況

final003

如上圖所示,我們使用 FinalUsageExtend 類繼承了 FinalUsage 類,並提供了 writeArticle 方法的重寫。這樣編譯是沒有問題的,重寫的關鍵點是 @Override 註解和方法修飾符、名稱、返回值的一致性。

注意:很多程式設計師在重寫方法的時候都會忽略 @Override,這樣其實無疑增加了程式碼閱讀的難度,不建議這樣。

當我們使用 final 修飾方法後,這個方法則不能被重寫,如下所示

final004

當我們把 writeArticle 方法宣告為 void 後,重寫的方法會報錯,無法重寫 writeArticle 方法。

final 可以修飾變數,final 修飾的變數一經定義後就不能被修改,如下所示

final005

編譯器提示的錯誤正是不能繼承一個被 final 修飾的類。

我們上面使用的是字串 String ,String 預設就是 final 的,其實用不用 final 修飾意義不大,因為字串本來就不能被改寫,這並不能說明問題。

我們改寫一下,使用基本資料型別來演示

final006

同樣的可以看到,編譯器仍然給出了 age 不能被改寫的提示,由此可以證明,final 修飾的變數不能被重寫。

在 Java 中不僅僅只有基本資料型別,還有引用資料型別,那麼引用型別被 final 修飾後會如何呢?我們看一下下面的程式碼

首先構造一個 Person

public class Person {
    int id;
    String name;
    get() and set() ...
    toString()...
} 

然後我們定義一個 final 的 Person 變數。

static final Person person = new Person(25,"cxuan");

public static void main(String[] args) {

  System.out.println(person);
  person.setId(26);
  person.setName("cxuan001");
  System.out.println(person);
} 

輸出一下,你會發現一個奇怪的現象,為什麼我們明明改了 person 中的 id 和 name ,編譯器卻沒有報錯呢?

這是因為,final 修飾的引用型別,只是保證物件的引用不會改變。物件內部的資料可以改變。這就涉及到物件在記憶體中的分配問題,我們後面再說。

finally 保證程式一定被執行

finally 是保證程式一定執行的機制,同樣的它也是 Java 中的一個關鍵字,一般來講,finally 一般不會單獨使用,它一般和 try 塊一起使用,例如下面是一段 try...finally 程式碼塊

try{
  lock.lock();
}finally {
  lock.unlock();
} 

這是一段加鎖/解鎖的程式碼示例,在 lock 加鎖後,在 finally 中執行解鎖操作,因為 finally 能夠保證程式碼一定被執行,所以一般都會把一些比較重要的程式碼放在 finally 中,例如解鎖操作、流關閉操作、連線釋放操作等。

當 lock.lock() 產生異常時還可以和 try...catch...finally 一起使用

try{
  lock.lock();
}catch(Exception e){
  e.printStackTrace();
}finally {
  lock.unlock();
} 

try...finally 這種寫法適用於 JDK1.7 之前,在 JDK1.7 中引入了一種新的關閉流的操作,那就是 try...with...resources,Java 引入了 try-with-resources 宣告,將 try-catch-finally 簡化為 try-catch,這其實是一種語法糖,並不是多了一種語法。try...with...resources 在編譯時還是會進行轉化為 try-catch-finally 語句。

語法糖(Syntactic sugar),也譯為糖衣語法,是指計算機語言中新增的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。通常來說使用語法糖能夠增加程式的可讀性,從而減少程式程式碼出錯的機會。

在 Java 中,有一些為了簡化程式設計師使用的語法糖,後面有機會我們再談。

finalize 的作用

finalize 是祖宗類 Object類的一個方法,它的設計目的是保證物件在垃圾收集前完成特定資源的回收。finalize 現在已經不再推薦使用,在 JDK 1.9 中已經明確的被標記為 deprecated

深入理解 final 、finally 和 finalize

final 設計

許多程式語言都會有某種方法來告知編譯器,某一塊資料是恆定不變的。有時候恆定不變的資料很有用,比如

  • 一個永不改變的編譯期常量 。例如 static final int num = 1024
  • 一個執行時被初始化的值,而且你不希望改變它

final 的設計會和 abstract 的設計產生衝突,因為 abstract 關鍵字主要修飾抽象類,而抽象類需要被具體的類所實現。final 表示禁止繼承,也就不會存在被實現的問題。因為只有繼承後,子類才可以實現父類的方法。

類中的所有 private 都隱式的指定為 final 的,在 private 修飾的程式碼中使用 final 並沒有額外的意義。

空白 final

Java 是允許空白 final 的,空白 final 指的是宣告為 final ,但是卻沒有對其賦值使其初始化。但是無論如何,編譯器都需要初始化 final,所以這個初始化的任務就交給了構造器來完成,空白 final 給 final 提供了更大的靈活性。如下程式碼

public class FinalTest {

   final Integer finalNum;
   
   public FinalTest(){
       finalNum = 11;
   }
   
   public FinalTest(int num){
       finalNum = num;
   }

    public static void main(String[] args) {
        new FinalTest();
        new FinalTest(25);
    }
} 

在不同的構造器中對不同的 final 進行初始化,使 finalNum 的使用更加靈活。

使用 final 的方法主要有兩個:不可變效率

  • 不可變:不可變說的是把方法鎖定(注意不是加鎖),重在防止其他方法重寫。
  • 效率:這個主要是針對 Java 早期版本來說的,在 Java 早期實現中,如果將方法宣告為 final 的,就是同意編譯器將對此方法的呼叫改為內嵌呼叫,但是卻沒有帶來顯著的效能優化。這種呼叫就比較雞肋,在 Java5/6 中,hotspot 虛擬機器會自動探測到內嵌呼叫,並把它們優化掉,所以使用 final 修飾的方法就主要有一個:不可變。
注意:final 不是 Immutable 的,Immutable 才是真正的不可變。

final 不是真正的 Immutable,因為 final 關鍵字引用的物件是可以改變的。如果我們真的希望物件不可變,通常需要相應的類支援不可變行為,比如下面這段程式碼

final List<String> fList = new ArrayList();
fList.add("Hello");
fList.add("World");
List unmodfiableList = List.of("hello","world");
unmodfiableList.add("again"); 

List.of 方法建立的就是不可變的 List。不可變 Immutable 在很多情況下是很好的選擇,一般來說,實現 Immutable 需要注意如下幾點

  • 將類宣告為 final,防止其他類進行擴充套件。
  • 將類內部的成員變數(包括例項變數和類變數)宣告為 privatefinal 的,不要提供可以修改成員變數的方法,也就是 setter 方法。
  • 在構造物件時,通常使用 deep-clone ,這樣有助於防止在直接對物件賦值時,其他人對輸入物件的修改。
  • 堅持 copy-on-write 原則,建立私有的拷貝。

final 能提高效能嗎?

final 能否提高效能一直是業界爭論的點,很多??書籍中都??介紹了可以在特定場景??提高效能,例如 final 可能用於幫助 JVM 將方法進行內聯,可以改造?編譯器進行?編譯?的能力等等,但這些結論很多都是基於假設作出的。

或許 R 大這篇回答會給我們一些結論 https://www.zhihu.com/question/21762917

大致說的就是無論區域性變數宣告時帶不帶 final 關鍵字修飾,對其訪問的效率都一樣

比如下面這段程式碼(不帶 final 的版本)

static int foo() {
  int a = someValueA();
  int b = someValueB();
  return a + b; // 這裡訪問區域性變數
} 

帶 final 的版本

static int foo() {
  final int a = someValueA();
  final int b = someValueB();
  return a + b; // 這裡訪問區域性變數
} 

使用 javac 編譯後得出來的結果一摸一樣。

invokestatic someValueA:()I
istore_0 // 設定a的值
invokestatic someValueB:()I
istore_1 // 設定b的值
iload_0  // 讀取a的值
iload_1  // 讀取b的值
iadd
ireturn 

因為上面是使用引用型別,所以位元組碼相同。

如果是常量型別,我們看一下

// 帶 final
static int foo(){

  final int a = 11;
  final int b = 12;

  return a + b;

}

// 不帶 final
static int foo(){

  int a = 11;
  int b = 12;

  return a + b;

} 

我們分別編譯一下兩個 foo 方法,會發現如下位元組碼

final007

左邊是非 final 關鍵字修飾的程式碼,右邊是有 final 關鍵字修飾的程式碼,對比這兩個位元組碼,可以得出如下結論。

  • 不管有沒有 final 修飾 ,int a = 11 或者 int a = 12 都當作常量看待。
  • 在 return 返回處,不加 final 的 a + b 會當作變數來處理;加 final 修飾的 a + b 會直接當作常量處理。
其實這種層面上的差異只對比較簡易的 JVM 影響較大,因為這樣的 VM 對直譯器的依賴較大,原本 Class 檔案裡的位元組碼是怎樣的它就怎麼執行;對高效能的 JVM(例如 HotSpot、J9 等)則沒啥影響。

所以,大部分 final 對效能優化的影響,可以直接忽略,我們使用 final 更多的考量在於其不可變性。

深入理解 finally

我們上面大致聊到了 finally 的使用,其作用就是保證在 try 塊中的程式碼執行完成之後,必然會執行 finally 中的語句。不管 try 塊中是否丟擲異常。

那麼下面我們就來深入認識一下 finally ,以及 finally 的位元組碼是什麼,以及 finally 究竟何時執行的本質。

  • 首先我們知道 finally 塊只會在 try 塊執行的情況下才執行,finally 不會單獨存在

這個不用再過多解釋,這是大家都知道的一條規則。finally 必須和 try 塊或者 try catch 塊一起使用。

  • 其次,finally 塊在離開 try 塊執行完成後或者 try 塊未執行完成但是接下來是控制轉移語句時(return/continue/break)在控制轉移語句之前執行

這一條其實是說明 finally 的執行時機的,我們以 return 為例來看一下是不是這麼回事。

如下這段程式碼

static int mayThrowException(){
  try{
    return 1;
  }finally {
    System.out.println("finally");
  }
}

public static void main(String[] args) {
  System.out.println(FinallyTest.mayThrowException());
} 

從執行結果可以證明是 finally 要先於 return 執行的。

當 finally 有返回值時,會直接返回。不會再去返回 try 或者 catch 中的返回值。

static int mayThrowException(){
  try{
    return 1;
  }finally {
    return 2;
  }
}

public static void main(String[] args) {
  System.out.println(FinallyTest.mayThrowException());
} 
  • 在執行 finally 語句之前,控制轉移語句會將返回值存在本地變數中

看下面這段程式碼

static int mayThrowException(){
  int i = 100;
  try {
    return i;
  }finally {
    ++i;
  }
}

public static void main(String[] args) {
  System.out.println(FinallyTest.mayThrowException());
} 

上面這段程式碼能夠說明 return i 是先於 ++i 執行的,而且 return i 會把 i 的值暫存,和 finally 一起返回。

finally 的本質

下面我們來看一段程式碼

public static void main(String[] args) {

  int a1 = 0;
  try {
    a1 = 1;
  }catch (Exception e){
    a1 = 2;
  }finally {
    a1 = 3;
  }

  System.out.println(a1);
} 

這段程式碼輸出的結果是什麼呢?答案是 3,為啥呢?

抱著疑問,我們先來看一下這段程式碼的位元組碼

final008

位元組碼的中文註釋我已經給你標出來了,這裡需要注意一下下面的 Exception table,Exception table 是異常表,異常表中每一個條目代表一個異常發生器,異常發生器由 From 指標,To 指標,Target 指標和應該捕獲的異常型別構成。

所以上面這段程式碼的執行路徑有三種

  • 如果 try 語句塊中出現了屬於 exception 及其子類的異常,則跳轉到 catch 處理
  • 如果 try 語句塊中出現了不屬於 exception 及其子類的異常,則跳轉到 finally 處理
  • 如果 catch 語句塊中新出現了異常,則跳轉到 finally 處理

聊到這裡,我們還沒說 finally 的本質到底是什麼,仔細觀察一下上面的位元組碼,你會發現其實 finally 會把 a1 = 3 的位元組碼 iconst_3 和 istore_1 放在 try 塊和 catch 塊的後面,所以上面這段程式碼就形同於

public static void main(String[] args) {

  int a1 = 0;
  try {
    a1 = 1;
        // finally a1 = 3
  }catch (Exception e){
    a1 = 2;
    // finally a1 = 3
  }finally {
    a1 = 3;
  }
  System.out.println(a1);
} 

上面中的 Exception table 是隻有 Throwable 的子類 exception 和 error 才會執行異常走查的異常表,正常情況下沒有 try 塊是沒有異常表的,下面來驗證一下

public static void main(String[] args) {
  int a1 = 1;
  System.out.println(a1);
} 

比如上面我們使用了一段非常簡單的程式來驗證,編譯後我們來看一下它的位元組碼

final009

可以看到,果然沒有異常表的存在。

finally 一定會執行嗎

上面我們討論的都是 finally 一定會執行的情況,那麼 finally 一定會被執行嗎?恐怕不是。

除了機房斷電、機房爆炸、機房進水、機房被雷劈、強制關機、拔電源之外,還有幾種情況能夠使 finally 不會執行。

  • 呼叫 System.exit 方法
  • 呼叫 Runtime.getRuntime().halt(exitStatus) 方法
  • JVM 當機(搞笑臉)
  • 如果 JVM 在 try 或 catch 塊中達到了無限迴圈(或其他不間斷,不終止的語句)
  • 作業系統是否強行終止了 JVM 程式;例如,在 UNIX 上執行 kill -9 pid
  • 如果主機系統當機;例如電源故障,硬體錯誤,作業系統當機等不會執行
  • 如果 finally 塊由守護程式執行緒執行,那麼所有非守護執行緒在 finally 呼叫之前退出。

finalize 真的沒用嗎

我們上面簡單介紹了一下 finalize 方法,並說明了它是一種不好的實踐。那麼 finalize 呼叫的時機是什麼?為什麼說 finalize 沒用呢?

我們知道,Java 與 C++ 一個顯著的區別在於 Java 能夠自動管理記憶體,在 Java 中,由於 GC 的自動回收機制,因而並不能保證 finalize 方法會被及時地執行(垃圾物件的回收時機具有不確定性),也不能保證它們會被執行。

也就是說,finalize 的執行時期不確定,我們並不能依賴於 finalize 方法幫我們進行垃圾回收,可能出現的情況是在我們耗盡資源之前,gc 卻仍未觸發,所以推薦使用資源用完即顯示釋放的方式,比如 close 方法。除此之外,finalize 方法也會生吞異常。

finalize 的工作方式是這樣的:一旦垃圾回收器準備好釋放物件佔用的儲存空間,將會首先呼叫 finalize 方法,並且在下一次垃圾回收動作發生時,才會真正回收物件佔用的記憶體。垃圾回收只與記憶體有關

我們在日常開發中並不提倡使用 finalize 方法,能用 finalize 方法的地方,使用 try...finally 會處理的更好。

你好,我是 cxuan,一枚技術人。我一共寫了六本 PDF

《Java 核心技術總結》
《HTTP 核心總結》
《程式設計師必知的基礎知識》
《作業系統核心總結》
《Java 核心基礎 2.0》
《Java 面試題總結》

現在我把百度連結給大家放出來了,大家可以點選下方的連結領取

連結: https://pan.baidu.com/s/1mYAeS9hIhdMFh2rF3FDk0A 密碼: p9rs

相關文章