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

        共 8471字,需瀏覽 17分鐘

         ·

        2020-11-11 13:48

        點(diǎn)擊藍(lán)色“Java建設(shè)者?”關(guān)注我喲

        加個“星標(biāo)”,及時閱讀最新技術(shù)文章


        這是Java建設(shè)者第125篇原創(chuàng)文章

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

        final、finally 和 finalize

        我相信在座的各位都是資深程序員,final 這種基礎(chǔ)關(guān)鍵字就不用多說了。不過,還是要照顧一下小白讀者,畢竟我們都是從小白走過來的嘛。

        final 修飾類、屬性和方法

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

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

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

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

        注意:很多程序員在重寫方法的時候都會忽略 @Override,這樣其實(shí)無疑增加了代碼閱讀的難度,不建議這樣。

        當(dāng)我們使用 final 修飾方法后,這個方法則不能被重寫,如下所示

        當(dāng)我們把 writeArticle 方法聲明為 void 后,重寫的方法會報錯,無法重寫 writeArticle 方法。

        final 可以修飾變量,final 修飾的變量一經(jīng)定義后就不能被修改,如下所示

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

        我們上面使用的是字符串 String ,String 默認(rèn)就是 final 的,其實(shí)用不用 final 修飾意義不大,因?yàn)樽址緛砭筒荒鼙桓膶?,這并不能說明問題。

        我們改寫一下,使用基本數(shù)據(jù)類型來演示

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

        在 Java 中不僅僅只有基本數(shù)據(jù)類型,還有引用數(shù)據(jù)類型,那么引用類型被 final 修飾后會如何呢?我們看一下下面的代碼

        首先構(gòu)造一個 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);
        }

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

        這是因?yàn)椋琭inal 修飾的引用類型,只是保證對象的引用不會改變。對象內(nèi)部的數(shù)據(jù)可以改變。這就涉及到對象在內(nèi)存中的分配問題,我們后面再說。

        finally 保證程序一定被執(zhí)行

        finally 是保證程序一定執(zhí)行的機(jī)制,同樣的它也是 Java 中的一個關(guān)鍵字,一般來講,finally 一般不會單獨(dú)使用,它一般和 try 塊一起使用,例如下面是一段 try...finally 代碼塊

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

        這是一段加鎖/解鎖的代碼示例,在 lock 加鎖后,在 finally 中執(zhí)行解鎖操作,因?yàn)?finally 能夠保證代碼一定被執(zhí)行,所以一般都會把一些比較重要的代碼放在 finally 中,例如解鎖操作、流關(guān)閉操作、連接釋放操作等。

        當(dāng) lock.lock() 產(chǎn)生異常時還可以和 try...catch...finally 一起使用

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

        try...finally 這種寫法適用于 JDK1.7 之前,在 JDK1.7 中引入了一種新的關(guān)閉流的操作,那就是 try...with...resources,Java 引入了 try-with-resources 聲明,將 try-catch-finally 簡化為 try-catch,這其實(shí)是一種語法糖,并不是多了一種語法。try...with...resources 在編譯時還是會進(jìn)行轉(zhuǎn)化為 try-catch-finally 語句。

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

        在 Java 中,有一些為了簡化程序員使用的語法糖,后面有機(jī)會我們再談。

        finalize 的作用

        finalize 是祖宗類 Object類的一個方法,它的設(shè)計目的是保證對象在垃圾收集前完成特定資源的回收。finalize 現(xiàn)在已經(jīng)不再推薦使用,在 JDK 1.9 中已經(jīng)明確的被標(biāo)記為 deprecated

        深入理解 final 、finally 和 finalize

        final 設(shè)計

        許多編程語言都會有某種方法來告知編譯器,某一塊數(shù)據(jù)是恒定不變的。有時候恒定不變的數(shù)據(jù)很有用,比如

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

        final 的設(shè)計會和 abstract 的設(shè)計產(chǎn)生沖突,因?yàn)?abstract 關(guān)鍵字主要修飾抽象類,而抽象類需要被具體的類所實(shí)現(xiàn)。final 表示禁止繼承,也就不會存在被實(shí)現(xiàn)的問題。因?yàn)橹挥欣^承后,子類才可以實(shí)現(xiàn)父類的方法。

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

        空白 final

        Java 是允許空白 final 的,空白 final 指的是聲明為 final ,但是卻沒有對其賦值使其初始化。但是無論如何,編譯器都需要初始化 final,所以這個初始化的任務(wù)就交給了構(gòu)造器來完成,空白 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);
        ????}
        }

        在不同的構(gòu)造器中對不同的 final 進(jìn)行初始化,使 finalNum 的使用更加靈活。

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

        • 不可變:不可變說的是把方法鎖定(注意不是加鎖),重在防止其他方法重寫。
        • 效率:這個主要是針對 Java 早期版本來說的,在 Java 早期實(shí)現(xiàn)中,如果將方法聲明為 final 的,就是同意編譯器將對此方法的調(diào)用改為內(nèi)嵌調(diào)用,但是卻沒有帶來顯著的性能優(yōu)化。這種調(diào)用就比較雞肋,在 Java5/6 中,hotspot 虛擬機(jī)會自動探測到內(nèi)嵌調(diào)用,并把它們優(yōu)化掉,所以使用 final 修飾的方法就主要有一個:不可變。

        注意:final 不是 Immutable 的,Immutable 才是真正的不可變。

        final 不是真正的 Immutable,因?yàn)?final 關(guān)鍵字引用的對象是可以改變的。如果我們真的希望對象不可變,通常需要相應(yīng)的類支持不可變行為,比如下面這段代碼

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

        List.of 方法創(chuàng)建的就是不可變的 List。不可變 Immutable 在很多情況下是很好的選擇,一般來說,實(shí)現(xiàn) Immutable 需要注意如下幾點(diǎn)

        • 將類聲明為 final,防止其他類進(jìn)行擴(kuò)展。
        • 將類內(nèi)部的成員變量(包括實(shí)例變量和類變量)聲明為 privatefinal 的,不要提供可以修改成員變量的方法,也就是 setter 方法。
        • 在構(gòu)造對象時,通常使用 deep-clone ,這樣有助于防止在直接對對象賦值時,其他人對輸入對象的修改。
        • 堅(jiān)持 copy-on-write 原則,創(chuàng)建私有的拷貝。

        final 能提高性能嗎?

        final 能否提高性能一直是業(yè)界爭論的點(diǎn),很多書籍中都介紹了可以在特定場景提高性能,例如 final 可能用于幫助 JVM 將方法進(jìn)行內(nèi)聯(lián),可以改造編譯器進(jìn)行編譯的能力等等,但這些結(jié)論很多都是基于假設(shè)作出的。

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

        大致說的就是無論局部變量聲明時帶不帶 final 關(guān)鍵字修飾,對其訪問的效率都一樣

        比如下面這段代碼(不帶 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 編譯后得出來的結(jié)果一摸一樣。

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

        因?yàn)樯厦媸鞘褂靡妙愋?,所以字?jié)碼相同。

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

        //?帶?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 方法,會發(fā)現(xiàn)如下字節(jié)碼

        左邊是非 final 關(guān)鍵字修飾的代碼,右邊是有 final 關(guān)鍵字修飾的代碼,對比這兩個字節(jié)碼,可以得出如下結(jié)論。

        • 不管有沒有 final 修飾 ,int a = 11 或者 int a = 12 都當(dāng)作常量看待。
        • 在 return 返回處,不加 final 的 a + b 會當(dāng)作變量來處理;加 final 修飾的 a + b 會直接當(dāng)作常量處理。

        其實(shí)這種層面上的差異只對比較簡易的 JVM 影響較大,因?yàn)檫@樣的 VM 對解釋器的依賴較大,原本 Class 文件里的字節(jié)碼是怎樣的它就怎么執(zhí)行;對高性能的 JVM(例如 HotSpot、J9 等)則沒啥影響。

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

        深入理解 finally

        我們上面大致聊到了 finally 的使用,其作用就是保證在 try 塊中的代碼執(zhí)行完成之后,必然會執(zhí)行 finally 中的語句。不管 try 塊中是否拋出異常。

        那么下面我們就來深入認(rèn)識一下 finally ,以及 finally 的字節(jié)碼是什么,以及 finally 究竟何時執(zhí)行的本質(zhì)。

        • 首先我們知道 finally 塊只會在 try 塊執(zhí)行的情況下才執(zhí)行,finally 不會單獨(dú)存在。

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

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

        這一條其實(shí)是說明 finally 的執(zhí)行時機(jī)的,我們以 return 為例來看一下是不是這么回事。

        如下這段代碼

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

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

        從執(zhí)行結(jié)果可以證明是 finally 要先于 return 執(zhí)行的。

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

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

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

        看下面這段代碼

        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 執(zhí)行的,而且 return i 會把 i 的值暫存,和 finally 一起返回。

        finally 的本質(zhì)

        下面我們來看一段代碼

        public?static?void?main(String[]?args)?{

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

        ??System.out.println(a1);
        }

        這段代碼輸出的結(jié)果是什么呢?答案是 3,為啥呢?

        抱著疑問,我們先來看一下這段代碼的字節(jié)碼

        字節(jié)碼的中文注釋我已經(jīng)給你標(biāo)出來了,這里需要注意一下下面的 Exception table,Exception table 是異常表,異常表中每一個條目代表一個異常發(fā)生器,異常發(fā)生器由 From 指針,To 指針,Target 指針和應(yīng)該捕獲的異常類型構(gòu)成。

        所以上面這段代碼的執(zhí)行路徑有三種

        • 如果 try 語句塊中出現(xiàn)了屬于 exception 及其子類的異常,則跳轉(zhuǎn)到 catch 處理
        • 如果 try 語句塊中出現(xiàn)了不屬于 exception 及其子類的異常,則跳轉(zhuǎn)到 finally 處理
        • 如果 catch 語句塊中新出現(xiàn)了異常,則跳轉(zhuǎn)到 finally 處理

        聊到這里,我們還沒說 finally 的本質(zhì)到底是什么,仔細(xì)觀察一下上面的字節(jié)碼,你會發(fā)現(xiàn)其實(shí) finally 會把 a1 = 3 的字節(jié)碼 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 才會執(zhí)行異常走查的異常表,正常情況下沒有 try ?塊是沒有異常表的,下面來驗(yàn)證一下

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

        比如上面我們使用了一段非常簡單的程序來驗(yàn)證,編譯后我們來看一下它的字節(jié)碼

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

        finally 一定會執(zhí)行嗎

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

        除了機(jī)房斷電、機(jī)房爆炸、機(jī)房進(jìn)水、機(jī)房被雷劈、強(qiáng)制關(guān)機(jī)、拔電源之外,還有幾種情況能夠使 finally 不會執(zhí)行。

        • 調(diào)用 System.exit 方法

        • 調(diào)用 Runtime.getRuntime().halt(exitStatus) 方法

        • JVM 宕機(jī)(搞笑臉)

        • 如果 JVM 在 try 或 catch 塊中達(dá)到了無限循環(huán)(或其他不間斷,不終止的語句)

        • 操作系統(tǒng)是否強(qiáng)行終止了 JVM 進(jìn)程;例如,在 UNIX 上執(zhí)行 kill -9 pid

        • 如果主機(jī)系統(tǒng)死機(jī);例如電源故障,硬件錯誤,操作系統(tǒng)死機(jī)等不會執(zhí)行

        • 如果 finally 塊由守護(hù)程序線程執(zhí)行,那么所有非守護(hù)線程在 finally 調(diào)用之前退出。

        finalize 真的沒用嗎

        我們上面簡單介紹了一下 finalize 方法,并說明了它是一種不好的實(shí)踐。那么 finalize 調(diào)用的時機(jī)是什么?為什么說 finalize 沒用呢?

        我們知道,Java 與 C++ 一個顯著的區(qū)別在于 Java 能夠自動管理內(nèi)存,在 Java 中,由于 GC 的自動回收機(jī)制,因而并不能保證 finalize 方法會被及時地執(zhí)行(垃圾對象的回收時機(jī)具有不確定性),也不能保證它們會被執(zhí)行。

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

        finalize 的工作方式是這樣的:一旦垃圾回收器準(zhǔn)備好釋放對象占用的存儲空間,將會首先調(diào)用 finalize 方法,并且在下一次垃圾回收動作發(fā)生時,才會真正回收對象占用的內(nèi)存。垃圾回收只與內(nèi)存有關(guān)

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


        瀏覽 120
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報
          
          

            1. 日本熟妇大荫蒂毛茸茸 | www.四虎av | 五月天激情亚洲 | 四虎影库男人天堂 | 九九自拍视频 |