Java語言有很多看起來很相似,但是用途卻完全不同的語言要素,這些內(nèi)容往往容易成為面試官考察你知識(shí)掌握程度的切入點(diǎn)。
今天,我要問你的是一個(gè)經(jīng)典的Java基礎(chǔ)題目,談?wù)刦inal、finally、 finalize有什么不同?
典型回答
final可以用來修飾類、方法、變量,分別有不同的意義,final修飾的class代表不可以繼承擴(kuò)展,final的變量是不可以修改的,而final的方法也是不可以重寫的(override)。
finally則是Java保證重點(diǎn)代碼一定要被執(zhí)行的一種機(jī)制。我們可以使用try-finally或者try-catch-finally來進(jìn)行類似關(guān)閉JDBC連接、保證unlock鎖等動(dòng)作。
finalize是基礎(chǔ)類java.lang.Object的一個(gè)方法,它的設(shè)計(jì)目的是保證對(duì)象在被垃圾收集前完成特定資源的回收。finalize機(jī)制現(xiàn)在已經(jīng)不推薦使用,并且在JDK 9開始被標(biāo)記為deprecated。
考點(diǎn)分析
這是一個(gè)非常經(jīng)典的Java基礎(chǔ)問題,我上面的回答主要是從語法和使用實(shí)踐角度出發(fā)的,其實(shí)還有很多方面可以深入探討,面試官還可以考察你對(duì)性能、并發(fā)、對(duì)象生命周期或垃圾收集基本過程等方面的理解。
推薦使用final關(guān)鍵字來明確表示我們代碼的語義、邏輯意圖,這已經(jīng)被證明在很多場景下是非常好的實(shí)踐,比如:
我們可以將方法或者類聲明為final,這樣就可以明確告知?jiǎng)e人,這些行為是不許修改的。
如果你關(guān)注過Java核心類庫的定義或源碼, 有沒有發(fā)現(xiàn)java.lang包下面的很多類,相當(dāng)一部分都被聲明成為final class?在第三方類庫的一些基礎(chǔ)類中同樣如此,這可以有效避免API使用者更改基礎(chǔ)功能,某種程度上,這是保證平臺(tái)安全的必要手段。
使用final修飾參數(shù)或者變量,也可以清楚地避免意外賦值導(dǎo)致的編程錯(cuò)誤,甚至,有人明確推薦將所有方法參數(shù)、本地變量、成員變量聲明成final。
final變量產(chǎn)生了某種程度的不可變(immutable)的效果,所以,可以用于保護(hù)只讀數(shù)據(jù),尤其是在并發(fā)編程中,因?yàn)槊鞔_地不能再賦值final變量,有利于減少額外的同步開銷,也可以省去一些防御性拷貝的必要。
final也許會(huì)有性能的好處,很多文章或者書籍中都介紹了可在特定場景提高性能,比如,利用final可能有助于JVM將方法進(jìn)行內(nèi)聯(lián),可以改善編譯器進(jìn)行條件編譯的能力等等。坦白說,很多類似的結(jié)論都是基于假設(shè)得出的,比如現(xiàn)代高性能JVM(如HotSpot)判斷內(nèi)聯(lián)未必依賴final的提示,要相信JVM還是非常智能的。類似的,final字段對(duì)性能的影響,大部分情況下,并沒有考慮的必要。
從開發(fā)實(shí)踐的角度,我不想過度強(qiáng)調(diào)這一點(diǎn),這是和JVM的實(shí)現(xiàn)很相關(guān)的,未經(jīng)驗(yàn)證比較難以把握。我的建議是,在日常開發(fā)中,除非有特別考慮,不然最好不要指望這種小技巧帶來的所謂性能好處,程序最好是體現(xiàn)它的語義目的。如果你確實(shí)對(duì)這方面有興趣,可以查閱相關(guān)資料,我就不再贅述了,不過千萬別忘了驗(yàn)證一下。
對(duì)于finally,明確知道怎么使用就足夠了。需要關(guān)閉的連接等資源,更推薦使用Java 7中添加的try-with-resources語句,因?yàn)橥ǔava平臺(tái)能夠更好地處理異常情況,編碼量也要少很多,何樂而不為呢。
另外,我注意到有一些常被考到的finally問題(也比較偏門),至少需要了解一下。比如,下面代碼會(huì)輸出什么?
try {
? // do something
? System.exit(1);
} finally{
? System.out.println(“Print from finally”);
}
上面finally里面的代碼可不會(huì)被執(zhí)行的哦,這是一個(gè)特例。
對(duì)于finalize,我們要明確它是不推薦使用的,業(yè)界實(shí)踐一再證明它不是個(gè)好的辦法,在Java 9中,甚至明確將Object.finalize()標(biāo)記為deprecated!如果沒有特別的原因,不要實(shí)現(xiàn)finalize方法,也不要指望利用它來進(jìn)行資源回收。
為什么呢?簡單說,你無法保證finalize什么時(shí)候執(zhí)行,執(zhí)行的是否符合預(yù)期。使用不當(dāng)會(huì)影響性能,導(dǎo)致程序死鎖、掛起等。
通常來說,利用上面的提到的try-with-resources或者try-finally機(jī)制,是非常好的回收資源的辦法。如果確實(shí)需要額外處理,可以考慮Java提供的Cleaner機(jī)制或者其他替代方法。接下來,我來介紹更多設(shè)計(jì)考慮和實(shí)踐細(xì)節(jié)。
知識(shí)擴(kuò)展
1.注意,final不是immutable!
我在前面介紹了final在實(shí)踐中的益處,需要注意的是,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這個(gè)引用不可以被賦值,但是strList對(duì)象行為不被final影響,添加元素等操作是完全正常的。如果我們真的希望對(duì)象本身是不可變的,那么需要相應(yīng)的類支持不可變的行為。在上面這個(gè)例子中,List.of方法創(chuàng)建的本身就是不可變List,最后那句add是會(huì)在運(yùn)行時(shí)拋出異常的。
Immutable在很多場景是非常棒的選擇,某種意義上說,Java語言目前并沒有原生的不可變支持,如果要實(shí)現(xiàn)immutable的類,我們需要做到:
將class自身聲明為final,這樣別人就不能擴(kuò)展來繞過限制了。
將所有成員變量定義為private和final,并且不要實(shí)現(xiàn)setter方法。
通常構(gòu)造對(duì)象時(shí),成員變量使用深度拷貝來初始化,而不是直接賦值,這是一種防御措施,因?yàn)槟銦o法確定輸入對(duì)象不被其他人修改。
如果確實(shí)需要實(shí)現(xiàn)getter方法,或者其他可能會(huì)返回內(nèi)部狀態(tài)的方法,使用copy-on-write原則,創(chuàng)建私有的copy。
這些原則是不是在并發(fā)編程實(shí)踐中經(jīng)常被提到?的確如此。
關(guān)于setter/getter方法,很多人喜歡直接用IDE一次全部生成,建議最好是你確定有需要時(shí)再實(shí)現(xiàn)。
2.finalize真的那么不堪?
前面簡單介紹了finalize是一種已經(jīng)被業(yè)界證明了的非常不好的實(shí)踐,那么為什么會(huì)導(dǎo)致那些問題呢?
finalize的執(zhí)行是和垃圾收集關(guān)聯(lián)在一起的,一旦實(shí)現(xiàn)了非空的finalize方法,就會(huì)導(dǎo)致相應(yīng)對(duì)象回收呈現(xiàn)數(shù)量級(jí)上的變慢,有人專門做過benchmark,大概是40~50倍的下降。
因?yàn)椋琭inalize被設(shè)計(jì)成在對(duì)象被垃圾收集前調(diào)用,這就意味著實(shí)現(xiàn)了finalize方法的對(duì)象是個(gè)“特殊公民”,JVM要對(duì)它進(jìn)行額外處理。finalize本質(zhì)上成為了快速回收的阻礙者,可能導(dǎo)致你的對(duì)象經(jīng)過多個(gè)垃圾收集周期才能被回收。
有人也許會(huì)問,我用System.runFinalization()告訴JVM積極一點(diǎn),是不是就可以了?也許有點(diǎn)用,但是問題在于,這還是不可預(yù)測、不能保證的,所以本質(zhì)上還是不能指望。實(shí)踐中,因?yàn)閒inalize拖慢垃圾收集,導(dǎo)致大量對(duì)象堆積,也是一種典型的導(dǎo)致OOM的原因。
從另一個(gè)角度,我們要確?;厥召Y源就是因?yàn)橘Y源都是有限的,垃圾收集時(shí)間的不可預(yù)測,可能會(huì)極大加劇資源占用。這意味著對(duì)于消耗非常高頻的資源,千萬不要指望finalize去承擔(dān)資源釋放的主要職責(zé),最多讓finalize作為最后的“守門員”,況且它已經(jīng)暴露了如此多的問題。這也是為什么我推薦,資源用完即顯式釋放,或者利用資源池來盡量重用。
finalize還會(huì)掩蓋資源回收時(shí)的出錯(cuò)信息,我們看下面一段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();
}
結(jié)合我上期專欄介紹的異常處理實(shí)踐,你認(rèn)為這段代碼會(huì)導(dǎo)致什么問題?
是的,你沒有看錯(cuò),這里的Throwable是被生吞了的!也就意味著一旦出現(xiàn)異常或者出錯(cuò),你得不到任何有效信息。況且,Java在finalize階段也沒有好的方式處理任何信息,不然更加不可預(yù)測。
3.有什么機(jī)制可以替換finalize嗎?
Java平臺(tái)目前在逐步使用java.lang.ref.Cleaner來替換掉原有的finalize實(shí)現(xiàn)。Cleaner的實(shí)現(xiàn)利用了幻象引用(PhantomReference),這是一種常見的所謂post-mortem清理機(jī)制。我會(huì)在后面的專欄系統(tǒng)介紹Java的各種引用,利用幻象引用和引用隊(duì)列,我們可以保證對(duì)象被徹底銷毀前做一些類似資源回收的工作,比如關(guān)閉文件描述符(操作系統(tǒng)有限的資源),它比finalize更加輕量、更加可靠。
吸取了finalize里的教訓(xùn),每個(gè)Cleaner的操作都是獨(dú)立的,它有自己的運(yùn)行線程,所以可以避免意外死鎖等問題。
實(shí)踐中,我們可以為自己的模塊構(gòu)建一個(gè)Cleaner,然后實(shí)現(xiàn)相應(yīng)的清理邏輯。下面是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();
? ? ? ? }
? ? }
注意,從可預(yù)測性的角度來判斷,Cleaner或者幻象引用改善的程度仍然是有限的,如果由于種種原因?qū)е禄孟笠枚逊e,同樣會(huì)出現(xiàn)問題。所以,Cleaner適合作為一種最后的保證手段,而不是完全依賴Cleaner進(jìn)行資源回收,不然我們就要再做一遍finalize的噩夢了。
我也注意到很多第三方庫自己直接利用幻象引用定制資源收集,比如廣泛使用的MySQL JDBC driver之一的mysql-connector-j,就利用了幻象引用機(jī)制。幻象引用也可以進(jìn)行類似鏈條式依賴關(guān)系的動(dòng)作,比如,進(jìn)行總量控制的場景,保證只有連接被關(guān)閉,相應(yīng)資源被回收,連接池才能創(chuàng)建新的連接。
另外,這種代碼如果稍有不慎添加了對(duì)資源的強(qiáng)引用關(guān)系,就會(huì)導(dǎo)致循環(huán)引用關(guān)系,前面提到的MySQL JDBC就在特定模式下有這種問題,導(dǎo)致內(nèi)存泄漏。上面的示例代碼中,將State定義為static,就是為了避免普通的內(nèi)部類隱含著對(duì)外部對(duì)象的強(qiáng)引用,因?yàn)槟菢訒?huì)使外部對(duì)象無法進(jìn)入幻象可達(dá)的狀態(tài)。
今天,我從語法角度分析了final、finally、finalize,并從安全、性能、垃圾收集等方面逐步深入,探討了實(shí)踐中的注意事項(xiàng),希望對(duì)你有所幫助。