Java 語言有很多看起來很相似,但是用途卻完全不同的語言要素,這些內容往往容易成為面試官考察你知識掌握程度的切入點。
今天,我要問你的是一個經典的 Java 基礎題目,談談 final、finally、 finalize 有什么不同?
## 典型回答
final 可以用來修飾類、方法、變量,分別有不同的意義,final 修飾的 class 代表不可以繼承擴展,final 的變量是不可以修改的,而 final 的方法也是不可以重寫的(override)。
finally 則是 Java 保證重點代碼一定要被執行的一種機制。我們可以使用 try-finally 或者 try-catch-finally 來進行類似關閉 JDBC 連接、保證 unlock 鎖等動作。
finalize 是基礎類 java.lang.Object 的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,并且在 JDK 9 開始被標記為 deprecated。
## 考點分析
這是一個非常經典的 Java 基礎問題,我上面的回答主要是從語法和使用實踐角度出發的,其實還有很多方面可以深入探討,面試官還可以考察你對性能、并發、對象生命周期或垃圾收集基本過程等方面的理解。
推薦使用 final 關鍵字來明確表示我們代碼的語義、邏輯意圖,這已經被證明在很多場景下是非常好的實踐,比如:
* 我們可以將方法或者類聲明為 final,這樣就可以明確告知別人,這些行為是不許修改的。
如果你關注過 Java 核心類庫的定義或源碼, 有沒有發現 java.lang 包下面的很多類,相當一部分都被聲明成為 final class?在第三方類庫的一些基礎類中同樣如此,這可以有效避免 API 使用者更改基礎功能,某種程度上,這是保證平臺安全的必要手段。
* 使用 final 修飾參數或者變量,也可以清楚地避免意外賦值導致的編程錯誤,甚至,有人明確推薦將所有方法參數、本地變量、成員變量聲明成 final。
* final 變量產生了某種程度的不可變(immutable)的效果,所以,可以用于保護只讀數據,尤其是在并發編程中,因為明確地不能再賦值 final 變量,有利于減少額外的同步開銷,也可以省去一些防御性拷貝的必要。
final 也許會有性能的好處,很多文章或者書籍中都介紹了可在特定場景提高性能,比如,利用 final 可能有助于 JVM 將方法進行內聯,可以改善編譯器進行條件編譯的能力等等。坦白說,很多類似的結論都是基于假設得出的,比如現代高性能 JVM(如 HotSpot)判斷內聯未必依賴 final 的提示,要相信 JVM 還是非常智能的。類似的,final 字段對性能的影響,大部分情況下,并沒有考慮的必要。
從開發實踐的角度,我不想過度強調這一點,這是和 JVM 的實現很相關的,未經驗證比較難以把握。我的建議是,在日常開發中,除非有特別考慮,不然最好不要指望這種小技巧帶來的所謂性能好處,程序最好是體現它的語義目的。如果你確實對這方面有興趣,可以查閱相關資料,我就不再贅述了,不過千萬別忘了驗證一下。
對于 finally,明確知道怎么使用就足夠了。需要關閉的連接等資源,更推薦使用 Java 7 中添加的 try-with-resources 語句,因為通常 Java 平臺能夠更好地處理異常情況,編碼量也要少很多,何樂而不為呢。
另外,我注意到有一些常被考到的 finally 問題(也比較偏門),至少需要了解一下。比如,下面代碼會輸出什么?
~~~
try {
// do something
System.exit(1);
} finally{
System.out.println(“Print from finally”);
}
~~~
上面 finally 里面的代碼可不會被執行的哦,這是一個特例。
對于 finalize,我們要明確它是不推薦使用的,業界實踐一再證明它不是個好的辦法,在 Java 9 中,甚至明確將 Object.finalize() 標記為 deprecated!如果沒有特別的原因,不要實現 finalize 方法,也不要指望利用它來進行資源回收。
為什么呢?簡單說,你無法保證 finalize 什么時候執行,執行的是否符合預期。使用不當會影響性能,導致程序死鎖、掛起等。
通常來說,利用上面的提到的 try-with-resources 或者 try-finally 機制,是非常好的回收資源的辦法。如果確實需要額外處理,可以考慮 Java 提供的 Cleaner 機制或者其他替代方法。接下來,我來介紹更多設計考慮和實踐細節。
## 知識擴展
1\. 注意,final 不是 immutable!
我在前面介紹了 final 在實踐中的益處,需要注意的是,**final 并不等同于 immutable**,比如下面這段代碼:
~~~
final List<String> strList = new ArrayList<>();
strList.add("Hello");
strList.add("world");
List<String> unmodifiableStrList = List.of("hello", "world");
unmodifiableStrList.add("again");
~~~
final 只能約束 strList 這個引用不可以被賦值,但是 strList 對象行為不被 final 影響,添加元素等操作是完全正常的。如果我們真的希望對象本身是不可變的,那么需要相應的類支持不可變的行為。在上面這個例子中,[List.of 方法](http://openjdk.java.net/jeps/269)創建的本身就是不可變 List,最后那句 add 是會在運行時拋出異常的。
Immutable 在很多場景是非常棒的選擇,某種意義上說,Java 語言目前并沒有原生的不可變支持,如果要實現 immutable 的類,我們需要做到:
* 將 class 自身聲明為 final,這樣別人就不能擴展來繞過限制了。
* 將所有成員變量定義為 private 和 final,并且不要實現 setter 方法。
* 通常構造對象時,成員變量使用深度拷貝來初始化,而不是直接賦值,這是一種防御措施,因為你無法確定輸入對象不被其他人修改。
* 如果確實需要實現 getter 方法,或者其他可能會返回內部狀態的方法,使用 copy-on-write 原則,創建私有的 copy。
這些原則是不是在并發編程實踐中經常被提到?的確如此。
關于 setter/getter 方法,很多人喜歡直接用 IDE 一次全部生成,建議最好是你確定有需要時再實現。
2.finalize 真的那么不堪?
前面簡單介紹了 finalize 是一種已經被業界證明了的非常不好的實踐,那么為什么會導致那些問題呢?
finalize 的執行是和垃圾收集關聯在一起的,一旦實現了非空的 finalize 方法,就會導致相應對象回收呈現數量級上的變慢,有人專門做過 benchmark,大概是 40~50 倍的下降。
因為,finalize 被設計成在對象**被垃圾收集前**調用,這就意味著實現了 finalize 方法的對象是個“特殊公民”,JVM 要對它進行額外處理。finalize 本質上成為了快速回收的阻礙者,可能導致你的對象經過多個垃圾收集周期才能被回收。
有人也許會問,我用 System.runFinalization?() 告訴 JVM 積極一點,是不是就可以了?也許有點用,但是問題在于,這還是不可預測、不能保證的,所以本質上還是不能指望。實踐中,因為 finalize 拖慢垃圾收集,導致大量對象堆積,也是一種典型的導致 OOM 的原因。
從另一個角度,我們要確保回收資源就是因為資源都是有限的,垃圾收集時間的不可預測,可能會極大加劇資源占用。這意味著對于消耗非常高頻的資源,千萬不要指望 finalize 去承擔資源釋放的主要職責,最多讓 finalize 作為最后的“守門員”,況且它已經暴露了如此多的問題。這也是為什么我推薦,**資源用完即顯式釋放,或者利用資源池來盡量重用**。
finalize 還會掩蓋資源回收時的出錯信息,我們看下面一段 JDK 的源代碼,截取自 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 在 finalize 階段也沒有好的方式處理任何信息,不然更加不可預測。
3\. 有什么機制可以替換 finalize 嗎?
Java 平臺目前在逐步使用 java.lang.ref.Cleaner 來替換掉原有的 finalize 實現。Cleaner 的實現利用了幻象引用(PhantomReference),這是一種常見的所謂 post-mortem 清理機制。我會在后面的專欄系統介紹 Java 的各種引用,利用幻象引用和引用隊列,我們可以保證對象被徹底銷毀前做一些類似資源回收的工作,比如關閉文件描述符(操作系統有限的資源),它比 finalize 更加輕量、更加可靠。
吸取了 finalize 里的教訓,每個 Cleaner 的操作都是獨立的,它有自己的運行線程,所以可以避免意外死鎖等問題。
實踐中,我們可以為自己的模塊構建一個 Cleaner,然后實現相應的清理邏輯。下面是 JDK 自身提供的樣例程序:
~~~
public class CleaningExample implements AutoCloseable {
// A cleaner, preferably one shared within a library
private static final Cleaner cleaner = <cleaner>;
static class State implements Runnable {
State(...) {
// initialize State needed for cleaning action
}
public void run() {
// cleanup action accessing State, executed at most once
}
}
private final State;
private final Cleaner.Cleanable cleanable
public CleaningExample() {
this.state = new State(...);
this.cleanable = cleaner.register(this, state);
}
public void close() {
cleanable.clean();
}
}
~~~
注意,從可預測性的角度來判斷,Cleaner 或者幻象引用改善的程度仍然是有限的,如果由于種種原因導致幻象引用堆積,同樣會出現問題。所以,Cleaner 適合作為一種最后的保證手段,而不是完全依賴 Cleaner 進行資源回收,不然我們就要再做一遍 finalize 的噩夢了。
我也注意到很多第三方庫自己直接利用幻象引用定制資源收集,比如廣泛使用的 MySQL JDBC driver 之一的 mysql-connector-j,就利用了幻象引用機制。幻象引用也可以進行類似鏈條式依賴關系的動作,比如,進行總量控制的場景,保證只有連接被關閉,相應資源被回收,連接池才能創建新的連接。
另外,這種代碼如果稍有不慎添加了對資源的強引用關系,就會導致循環引用關系,前面提到的 MySQL JDBC 就在特定模式下有這種問題,導致內存泄漏。上面的示例代碼中,將 State 定義為 static,就是為了避免普通的內部類隱含著對外部對象的強引用,因為那樣會使外部對象無法進入幻象可達的狀態。
今天,我從語法角度分析了 final、finally、finalize,并從安全、性能、垃圾收集等方面逐步深入,探討了實踐中的注意事項,希望對你有所幫助。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?也許你已經注意到了,JDK 自身使用的 Cleaner 機制仍然是有缺陷的,你有什么更好的建議嗎?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?