Java 中的 final、finally、finalize 有什麼不一樣?

Java 中 final、finally、finalize 有什麼不一樣?這是在 Java 面試中常常問到的問題,他們究竟有什麼不一樣呢?java

這三個看起來很類似,其實他們的關係就像卡巴斯基和巴基斯坦同樣有基巴關係。面試

那麼若是被問到這個問題該怎麼回答呢?首先能夠從語法和使用角度出發簡單介紹三者的不一樣:編程

  • final 能夠用來修飾類、方法、變量,分別有不一樣的意義,final 修飾的 class 表明不能夠繼承擴展,final 的變量是不能夠修改的,而 final 的方法也是不能夠重寫的(override)。
  • finally 是 Java 保證重點代碼必定要被執行的一種機制。可使用 try-finally 或者 try-catch-finally 來進行相似關閉 JDBC 鏈接、保證 unlock 鎖等動做。
  • finalize 是基礎類 java.lang.Object 的一個方法,設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制如今已經不推薦使用,而且在 JDK 9 開始被標記爲 deprecated。

若是隻回答到這裏,就會沒有亮點,咱們能夠再深刻地去介紹三者的不一樣,好比從性能、併發、對象生命週期或垃圾收集基本過程等方面去談談本身的理解。安全

final

使用 final 關鍵字能夠明確表示代碼的語義、邏輯意圖,好比:併發

能夠將方法或者類聲明爲 final,這樣就能夠明確告知別人,這些行爲是不準修改的。Java 核心類庫的定義或源碼,好比 java.lang 包下面的不少類,至關一部分都被聲明成爲 final class,好比咱們常見的 String 類,在第三方類庫的一些基礎類中一樣如此,這能夠有效避免 API 使用者更改基礎功能,某種程度上,這是保證平臺安全的必要手段。ide

使用 final 修飾參數或者變量,也能夠清楚地避免意外賦值致使的編程錯誤,甚至,有人明確推薦將全部方法參數、本地變量、成員變量聲明成 final。post

final 變量產生了某種程度的不可變(immutable)的效果,因此,能夠用於保護只讀數據,尤爲是在併發編程中,由於明確地不能再賦值 final 變量,有利於減小額外的同步開銷,也能夠省去一些防護性拷貝的必要。性能

關於 final 也許會有性能的好處,不少文章或者書籍中都介紹了可在特定場景提升性能,好比,利用 final 可能有助於 JVM 將方法進行內聯,能夠改善編譯器進行條件編譯的能力等等。我在以前一篇文章進行了介紹,想了解的能夠點擊查閱。this

擴展閱讀:深刻理解 Java 中的 final 關鍵字spa

final 與 immutable

在前面介紹了 final 在實踐中的益處,須要注意的是,final 並不等同於 immutable,好比下面這段代碼:

final List<String> strList = new ArrayList<>();
strList.add("wupx");
strList.add("huxy");  
List<String> loveList = List.of("wupx", "huxy");
loveList.add("love");複製代碼

final 只能約束 strList 這個引用不能夠被賦值,可是 strList 對象行爲不被 final 影響,添加元素等操做是徹底正常的。若是咱們真的但願對象自己是不可變的,那麼須要相應的類支持不可變的行爲。在上面這個例子中,List.of 方法建立的自己就是不可變 List,最後那句 add 是會在運行時拋出異常的。

Immutable 在不少場景是很是棒的選擇,某種意義上說,Java 語言目前並無原生的不可變支持,若是要實現 immutable 的類,咱們須要作到:

將 class 自身聲明爲 final,這樣別人就不能擴展來繞過限制了。

將全部成員變量定義爲 private 和 final,而且不要實現 setter 方法。

一般構造對象時,成員變量使用深度拷貝來初始化,而不是直接賦值,這是一種防護措施,由於你沒法肯定輸入對象不被其餘人修改。

若是確實須要實現 getter 方法,或者其餘可能會返回內部狀態的方法,使用 copy-on-write 原則,建立私有的 copy。

關於 setter/getter 方法,不少人喜歡直接用 IDE 或者 Lombok 一次所有生成,建議最好肯定有須要時再實現。

finally

對於 finally,知道怎麼使用就足夠了。須要關閉的鏈接等資源,更推薦使用 Java 7 中添加的 try-with-resources 語句,由於一般 Java 平臺可以更好地處理異常狀況,還能夠減小代碼量。

另外,有一些常被考到的 finally 問題。好比,下面代碼會輸出什麼?

try {
  // do something
  System.exit(1);
} finally{
  System.out.println("Hello,I am finally。");
}複製代碼

上面 finally 裏面的代碼是不會被執行的,由於 try-catch 異常退出了。

像其餘 finally 中的代碼不會執行的狀況還有:

// 死循環
try{
    while(ture){
        System.out.println("always run");
    }
}finally{
    System.out.println("ummm");
}

// 線程被殺死
當執行 try-finally 的線程被殺死時,finally 中的代碼也沒法執行。複製代碼

finalize

對於 finalize,是不推薦使用的,在 Java 9 中,已經將 Object.finalize() 標記爲 deprecated。

爲何呢?由於沒法保證 finalize 何時執行,執行的是否符合預期。使用不當會影響性能,致使程序死鎖、掛起等。

一般來講,利用上面的提到的 try-with-resources 或者 try-finally 機制,是很是好的回收資源的辦法。若是確實須要額外處理,能夠考慮 Java 提供的 Cleaner 機制或者其餘替代方法。

爲何不推薦使用 finalize?

前面簡單介紹了 finalize 是不推薦使用的,究竟爲何不推薦使用呢?

  1. finalize 的執行是和垃圾收集關聯在一塊兒的,一旦實現了非空的 finalize 方法,就會致使相應對象回收呈現數量級上的變慢。
  2. finalize 被設計成在對象被垃圾收集前調用,JVM 要對它進行額外處理。finalize 本質上成爲了快速回收的阻礙者,可能致使對象通過多個垃圾收集週期才能被回收。
  3. finalize 拖慢垃圾收集,致使大量對象堆積,也是一種典型的致使 OOM 的緣由。
  4. 要確保回收資源就是由於資源都是有限的,垃圾收集時間的不可預測,可能會極大加重資源佔用。
  5. finalize 會掩蓋資源回收時的出錯信息。

所以對於消耗很是高頻的資源,千萬不要期望 finalize 去承擔資源釋放的主要職責。建議資源用完即顯式釋放,或者利用資源池來儘可能重用。

下面給出 finalize 掩蓋資源回收時的出錯信息的例子,讓咱們來看 java.lang.ref.Finalizer 的源代碼:

private void runFinalizer(JavaLangAccess jla) {
    //  ... 省略部分代碼
    try {
        Object finalizee = this.get(); 
        if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
           jla.invokeFinalize(finalizee);
           // Clear stack slot containing this variable, to decrease
           // the chances of false retention with a conservative GC
           finalizee = null;
        }
    } catch (Throwable x) { }
        super.clear(); 
}複製代碼

看過以前講解異常文章的朋友,應該能夠很快看出 Throwable 是被吞掉的,也就意味着一旦出現異常或者出錯,得不到任何有效信息。

擴展閱讀:Java 異常處理的 20 個最佳實踐,你知道幾個?

有更好的方法替代 finalize 嗎?

Java 平臺目前在逐步使用 java.lang.ref.Cleaner 來替換掉原有的 finalize 實現。Cleaner 的實現利用了幻象引用(PhantomReference),這是一種常見的所謂 post-mortem 清理機制。利用幻象引用和引用隊列,能夠保證對象被完全銷燬前作一些相似資源回收的工做,好比關閉文件描述符(操做系統有限的資源),它比 finalize 更加輕量、更加可靠。

每一個 Cleaner 的操做都是獨立的,有本身的運行線程,因此能夠避免意外死鎖等問題。

咱們能夠爲本身的模塊構建一個 Cleaner,而後實現相應的清理邏輯,具體代碼以下:

/**
 * Cleaner 是一個用於關閉資源的類,功能相似 finalize 方法
 * Cleaner 有本身的線程,在全部清理操做完成後,本身會被 GC
 * 清理中拋出的異常會被忽略
 * 
 * 清理方法(一個 Runnable)只會運行一次。會在兩種狀況下運行:
 * 1. 註冊的 Object 處於幻象引用狀態
 * 2. 顯式調用 clean 方法
 * 
 * 經過幻象引用和引用隊列實現
 * 能夠註冊多個對象,一般被定義爲靜態(減小線程數量)
 * 註冊對象後返回的Cleanable對象用於顯式調用 clean 方法
 * 實現清理行爲的對象(下面的 state),不能擁有被清理對象的引用
 * 若是將下面的 State 類改成非靜態,第二個 CleaningExample 將不會被 clean,
 * 由於非靜態內部類持有外部對象的引用,外部對象沒法進入幻象引用狀態
 */
public class CleaningExample implements AutoCloseable {

    public static void main(String[] args) {
        try {
            // 使用JDK7的try with Resources顯式調用clean方法
            try (CleaningExample ignored = new CleaningExample()) {
                throw new RuntimeException();
            }
        } catch (RuntimeException ignored) {
        }

        // 經過GC調用clean方法
        new CleaningExample();
        System.gc();
    }

    private static final Cleaner CLEANER = Cleaner.create();

    // 若是是非靜態內部類,則會出錯
    static class State implements Runnable {
        State() {
        }

        @Override
        public void run() {
            System.out.println("Cleaning called");
        }
    }

    private final State state;
    private final Cleaner.Cleanable cleanable;

    public CleaningExample() {
        this.state = new State();
        this.cleanable = CLEANER.register(this, state);
    }

    @Override
    public void close() {
        cleanable.clean();
    }

}複製代碼

其中,將 State 定義爲 static,就是爲了不普通的內部類隱含着對外部對象的強引用,由於那樣會使外部對象沒法進入幻象可達的狀態。

從可預測性的角度來判斷,Cleaner 或者幻象引用改善的程度仍然是有限的,若是因爲種種緣由致使幻象引用堆積,一樣會出現問題。因此,Cleaner 適合做爲一種最後的保證手段,而不是徹底依賴 Cleaner 進行資源回收。

總結

這篇文章首先從從語法角度分析了 final、finally、finalize,並從安全、性能、垃圾收集等方面逐步深刻,詳細地講解了 final、finally、finalize 三者的區別。file

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索