引子:一說到final關鍵字,相信大家都會立刻想起一些基本的作用,那么我們先稍微用寥寥數行來回顧一下。
一、final關鍵字的含義
final是Java中的一個保留關鍵字,它可以標記在成員變量、方法、類以及本地變量上。一旦我們將某個對象聲明為了final的,那么我們將不能再改變這個對象的引用了。如果我們嘗試將被修飾為final的對象重新賦值,編譯器就會報錯。
二、用法
1.修飾變量
final修飾在成員變量或者局部變量上,那么我們可以稱這個變量是final變量,這可能使我們用到最多的地方,舉個栗子:常量(雖然現在建議使用枚舉類來代替常量)。
如果我們將被final修飾的變量重新賦值,編譯器就會報出如圖:cannot assign a value to final variable.(不能給final變量賦值)
雖然我們不能改變對象的引用,但是我們仍舊可以set對象的屬性。
例如:
我們有一個People類:
class People { private String name; private Integer age; // getter and setter }
我們來測試一下:
public class Test { final People people = new People(); people.setAge(20); people.setName("Mike"); // 不報錯,可以給對象里面設值 //people = new People(); // 改變引用將會報錯,同上一樣 }
以上結論適用於集合類。
2.修飾方法
被final所修飾的方法將無法被子類重寫。
“使用final方法的原因有兩個。第一個原因是把方法鎖定,以防任何繼承類修改它的含義;第二個原因是效率。在早期的Java實現版本中,會將final方法轉為內嵌調用。但是如果方法過於龐大,可能看不到內嵌調用帶來的任何性能提升。在最近的Java版本中,不需要使用final方法進行這些優化了。” -- 摘自《Java編程思想》
因此如果你認為一個方法的功能已經足夠完整了,子類中不需要改變的話,你可以聲明此方法為final。final方法比非final方法要快,因為在編譯的時候已經靜態綁定了,不需要在運行時再動態綁定(正如編程思想中所提到的,在現在幾版較新的JDK中,已經幾乎沒有性能差別了)。
(當我們嘗試重寫的時候編譯器就會報錯)。
注:類的private方法會隱式地被指定為final方法。
3.修飾類
如果某個類被final所修飾,那么表明這個的功能通常是完整的;該類將不能被繼承。並且final類的所有方法都會被隱式的修飾成final。
4.ps:匿名類中的所有變量都必須是final的。
三、關鍵字final的好處小結
- final關鍵字提高了性能。JVM和Java應用都會緩存final變量。
- final變量可以安全的在多線程環境下進行共享,而不需要額外的同步開銷。
- 使用final關鍵字,JVM會對方法、變量及類進行優化。
- 對於不可變類,它的對象是只讀的,可以在多線程環境下安全的共享,不用額外的同步開銷。
四、來自《Effective Java》中的一些建議
該書的第17條:要么為了繼承而設計,並提供文檔說明,要么就禁止繼承。
該條目提醒我們,如果類不是被設計用來繼承的,那么這個類就應該被禁止繼承(聽起來有點繞,但細想下來的設計思想是很好的),否則就應該提供足夠的文檔及注釋(具體可參考java.util.AbstractCollection這個骨架實現里的注釋文檔規范)。
而禁止類被子類化的方法通常有兩個:
1.將所有的構造器設為私有的(private)或者包級私有的(default),並使用靜態工廠方法來代替構造器;
2.將類標記為final。
五、思考
1.一些思考回頭再來審視我們日常中的程序,我們可能已經習慣了不去那么刻意的使用final,頂多在寫常量的時候用一用,但實際上我們很多的類,方法或者變量是不需要被改變的,或者說不會被繼承的。比如我在剛讀到《Effective Java》中的這個條目后,回首自己正在做的一個項目中審視了一下,我首先將自己的domain層中的一些類標為了final,因為我覺得這些類是不可能被繼承的,如果繼承了是不太符合設計的,並且程序運行沒有異常,同時修改的還有我的依賴注入方式(參考我的上一篇博客:Spring注解依賴注入的三種方式的優缺點以及優先選擇)
我重新糾正了一下自己在設計類的時候的思想順序:之前自己在准備寫一個類的時候(雖然通常我是不給類加final的= =),可能覺得這個類(變量或者方法)不能被改變,有很強烈的這種想法時才會加上final,但現在是:這個類需不需要使他可以被子類化?如果在以后的項目更新,迭代中,並不需要,那么我會毫不猶豫的給他加上final。
2."final關鍵字能提升性能"?
當時發現這一點之后,我可能是中毒了,給能加上final的地方都加上了,自以為改善了性能心里還美滋滋呢。其實對這個“提升性能”一點一直還有一絲的疑問,於是我回頭就去了Stack Overflow上轉了一圈,找到了我想要的答案:Does use of final keyword in Java improve the performance?
大佬指出,通常是不會的,對於方法,HotPot會跟蹤看它是否真的被重寫了,並且能夠優化沒有被重寫的內聯方法,直到它加載到了一個類復寫了這個方法,這時它可以撤銷(或部分撤銷)這些優化。(當然,這是假設您使用的是HotPot,但到目前為止這是最常見的JVM,所以…)
之后大佬指出了我們不應該為了這么絲許的性能而絞盡腦汁,建議我們應該明確設計,寫出好的結構的代碼以及可讀性優良的代碼。(在此又應證了《Effective Java》中的第55條:謹慎地進行優化中所指出的核心:優化的格言就是:不要進行優化) (也驗證了上面《Java編程思想》中最后的那句話)
(ps: 原諒我翻譯一般,英語好的可以點去原文看~)
3.關於局部變量以及參數中的final
接着我嘗試將我的局部變量以及方法中的參數都標記為final的,同2一樣,已經中毒頗深了。但是我對此同時也存在着同樣的疑問,然后在Stack Overflow中得到了經驗證的又一個結論:局部變量以及參數中的final,同樣不能提升我們的性能,它甚至不會被寫進字節碼中。於是我操起了鍵盤啪啪啪一頓敲了幾行代碼編譯了一下,並用反編譯工具(如JD-GUI)打開:
先來看我們的源碼:
public class FinalTest { private static void say(final int number) { System.out.println("number: " + number); } public static void main(String[] args) { final int num = 0; say(num); } }
再來看看編譯后的.class文件:
public class FinalTest { public FinalTest() { } private static void say(int number) { System.out.println("number: " + number); } public static void main(String[] args) { int num = false; say(0); } }
可以看到在寫入字節碼的時候就被優化掉了,final只是編譯時靜態限制我們不能再賦值(改變引用)。
---------------------------- 2019年1月16日 更新 ----------------------------
【阿里Java編碼規約】
18.【推薦】final可提高程序響應效率,聲明成final的情況:
(1)不需要重新賦值的變量,包括類屬性、局部變量;
(2)對象參數前加final,表示不允許修改引用的指向;
(3)類方法確定不允許被重寫。
- 所有JVM都相關:方法參數與局部變量用final修飾是純編譯時信息,到Class文件里就已經沒有蹤跡了,JVM根本不會知道方法參數或者局部變量有沒有被final修飾。這個是完全不可能影響性能的。
- HotSpot JVM相關:
- static final常量是好的,HotSpot VM里的JIT編譯器會利用這個信息來做優化;
- 實例變量(字段)用final修飾的話,目前(直到最新的JDK8u)的HotSpot VM都不會使用這個信息來優化。只有JDK自身的一些核心類被區別對待(例如說java.lang.String上的final成員)。所以這個也不會影響性能。
- 實例方法用final修飾:其實在可以使用final修飾的場景下,就算沒有用final修飾,HotSpot VM的JIT編譯器也一樣會通過<b>類層次分析</b>(Class Hierarchy Analysis,CHA)來發現這個方法是實質上只有一個實現版本的,所以最終生成的代碼質量會一樣,性能不會有區別。而在不能使用final修飾的場景下(例如說這個方法真的有多個實現),如果當前加載的類中其實只有單一實現(別的實現在尚未加載或已經卸載的類中),則HotSpot VM仍然可以通過CHA把它當作單一實現來做激進優化,並通過deoptimization來保證安全。所以在HotSpot VM的環境中,其實實例方法沒有必要為了性能而加final,而是真的希望表達語義上的限制(例如java.lang.getClass()是一個不允許覆寫的方法,加final修飾就很合理)時才應該使用。
所以JVM發展到現在,我們聲明 final 的目的已經不再主要是性能問題了,而是正確性、合理性、嚴謹性的問題。用來提醒自己以及其他人,這里的參數/變量是真的不能被修改,並讓Java編譯器去檢查到底有沒有被亂改