溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何理解final、finally和finalize

發(fā)布時(shí)間:2021-10-25 16:13:48 來源:億速云 閱讀:141 作者:iii 欄目:編程語言

本篇內(nèi)容主要講解“如何理解final、finally和finalize”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“如何理解final、finally和finalize”吧!

如何理解final、finally和finalize

1. final、finally 和 finalize

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

(1) final 修飾類、屬性和方法

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

如何理解final、finally和finalize

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

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

如何理解final、finally和finalize

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

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

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

如何理解final、finally和finalize

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

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

如何理解final、finally和finalize

編譯器提示的錯(cuò)誤正是不能繼承一個(gè)被 final 修飾的類。

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

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

如何理解final、finally和finalize

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

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

首先構(gòu)造一個(gè) Person 類:

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

然后我們定義一個(gè) 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)一個(gè)奇怪的現(xiàn)象,為什么我們明明改了 person 中的 id 和 name ,編譯器卻沒有報(bào)錯(cuò)呢?

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

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

finally 是保證程序一定執(zhí)行的機(jī)制,同樣的它也是 Java 中的一個(gè)關(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)生異常時(shí)還可以和 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 在編譯時(shí)還是會進(jìn)行轉(zhuǎn)化為  try-catch-finally 語句。

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

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

(3) finalize 的作用

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

2. 深入理解 final 、finally 和 finalize

(1) final 設(shè)計(jì)

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

  • 一個(gè)永不改變的編譯期常量 。例如 static final int num = 1024

  • 一個(gè)運(yùn)行時(shí)被初始化的值,而且你不希望改變它

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

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

(2) 空白 final

Java 是允許空白 final 的,空白 final 指的是聲明為 final ,但是卻沒有對其賦值使其初始化。但是無論如何,編譯器都需要初始化  final,所以這個(gè)初始化的任務(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 的方法主要有兩個(gè):不可變 和 效率

  • 不可變:不可變說的是把方法鎖定(注意不是加鎖),重在防止其他方法重寫。

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

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

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

final List<String> 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í)例變量和類變量)聲明為 private 或 final 的,不要提供可以修改成員變量的方法,也就是 setter 方法。

  • 在構(gòu)造對象時(shí),通常使用 deep-clone ,這樣有助于防止在直接對對象賦值時(shí),其他人對輸入對象的修改。

  • 堅(jiān)持 copy-on-write 原則,創(chuàng)建私有的拷貝。

(3) final 能提高性能嗎?

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

大致說的就是無論局部變量聲明時(shí)帶不帶 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;  }

我們分別編譯一下兩個(gè) foo 方法,會發(fā)現(xiàn)如下字節(jié)碼:

如何理解final、finally和finalize

左邊是非 final 關(guān)鍵字修飾的代碼,右邊是有 final 關(guān)鍵字修飾的代碼,對比這兩個(gè)字節(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 更多的考量在于其不可變性。

(4) 深入理解 finally

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

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

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

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

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

這一條其實(shí)是說明 finally 的執(zhí)行時(shí)機(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 有返回值時(shí),會直接返回。不會再去返回 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 一起返回。

(5) 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é)碼

如何理解final、finally和finalize

字節(jié)碼的中文注釋我已經(jīng)給你標(biāo)出來了,這里需要注意一下下面的 Exception table,Exception table  是異常表,異常表中每一個(gè)條目代表一個(gè)異常發(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é)碼

如何理解final、finally和finalize

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

(6) 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ī);例如電源故障,硬件錯(cuò)誤,操作系統(tǒng)死機(jī)等不會執(zhí)行

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

(7) finalize 真的沒用嗎

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

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

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

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

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

到此,相信大家對“如何理解final、finally和finalize”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI