final使用html
final關鍵字的最佳實踐java
微信公衆號程序員
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到個人倉庫裏查看github
https://github.com/h2pl/Java-...
喜歡的話麻煩點下Star哈面試
文章首發於個人我的博客:算法
www.how2playlife.com
本文是微信公衆號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部份內容來源於網絡,爲了把本文主題講得清晰透徹,也整合了不少我認爲不錯的技術博客內容,引用其中了一些比較好的博客文章,若有侵權,請聯繫做者。
該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,並上手進行實戰,接着瞭解每一個Java知識點背後的實現原理,更完整地瞭解整個Java技術體系,造成本身的知識框架。爲了更好地總結和檢驗你的學習成果,本系列文章也會提供每一個知識點對應的面試題以及參考答案。segmentfault
若是對本系列文章有什麼建議,或者是有什麼疑問的話,也能夠關注公衆號【Java技術江湖】聯繫做者,歡迎你參與本系列博文的創做和修訂。後端
<!-- more -->
final關鍵字在java中使用很是普遍,能夠申明成員變量、方法、類、本地變量。一旦將引用聲明爲final,將沒法再改變這個引用。final關鍵字還能保證內存同步,本博客將會從final關鍵字的特性到從java內存層面保證同步講解。這個內容在面試中也有可能會出現。數組
final變量有成員變量或者是本地變量(方法內的局部變量),在類成員中final常常和static一塊兒使用,做爲類常量使用。其中類常量必須在聲明時初始化,final成員常量能夠在構造函數初始化。
public class Main { public static final int i; //報錯,必須初始化 由於常量在常量池中就存在了,調用時不須要類的初始化,因此必須在聲明時初始化 public static final int j; Main() { i = 2; j = 3; } }
就如上所說的,對於類常量,JVM會緩存在常量池中,在讀取該變量時不會加載這個類。
public class Main { public static final int i = 2; Main() { System.out.println("調用構造函數"); // 該方法不會調用 } public static void main(String[] args) { System.out.println(Main.i); } }
@Test public void final修飾基本類型變量和引用() { final int a = 1; final int[] b = {1}; final int[] c = {1}; // b = c;報錯 b[0] = 1; final String aa = "a"; final Fi f = new Fi(); //aa = "b";報錯 // f = null;//報錯 f.a = 1; }
final方法表示該方法不能被子類的方法重寫,將方法聲明爲final,在編譯的時候就已經靜態綁定了,不須要在運行時動態綁定。final方法調用時使用的是invokespecial指令。
class PersonalLoan{ public final String getName(){ return"personal loan」; } } class CheapPersonalLoan extends PersonalLoan{ @Override public final String getName(){ return"cheap personal loan";//編譯錯誤,沒法被重載 } public String test() { return getName(); //能夠調用,由於是public方法 } }
final類不能被繼承,final類中的方法默認也會是final類型的,java中的String類和Integer類都是final類型的。
class Si{ //通常狀況下final修飾的變量必定要被初始化。 //只有下面這種狀況例外,要求該變量必須在構造方法中被初始化。 //而且不能有空參數的構造方法。 //這樣就可讓每一個實例都有一個不一樣的變量,而且這個變量在每一個實例中只會被初始化一次 //因而這個變量在單個實例裏就是常量了。 final int s ; Si(int s) { this.s = s; } } class Bi { final int a = 1; final void go() { //final修飾方法沒法被繼承 } } class Ci extends Bi { final int a = 1; // void go() { // //final修飾方法沒法被繼承 // } } final char[]a = {'a'}; final int[]b = {1};
final class PersonalLoan{} class CheapPersonalLoan extends PersonalLoan { //編譯錯誤,沒法被繼承 }
@Test public void final修飾類() { //引用沒有被final修飾,因此是可變的。 //final只修飾了Fi類型,即Fi實例化的對象在堆中內存地址是不可變的。 //雖然內存地址不可變,可是能夠對內部的數據作改變。 Fi f = new Fi(); f.a = 1; System.out.println(f); f.a = 2; System.out.println(f); //改變實例中的值並不改變內存地址。 Fi ff = f; //讓引用指向新的Fi對象,原來的f對象由新的引用ff持有。 //引用的指向改變也不會改變原來對象的地址 f = new Fi(); System.out.println(f); System.out.println(ff); }
final方法的好處:
一、final 對於常量來講,意味着值不能改變,例如 final int i=100。這個i的值永遠都是100。
可是對於變量來講又不同,只是標識這個引用不可被改變,例如 final File f=new File("c:\test.txt");
那麼這個f必定是不能被改變的,若是f自己有方法修改其中的成員變量,例如是否可讀,是容許修改的。有個形象的比喻:一個女子定義了一個final的老公,這個老公的職業和收入都是容許改變的,只是這個女人不會換老公而已。
final修飾的變量有三種:靜態變量、實例變量和局部變量,分別表示三種類型的常量。
另外,final變量定義的時候,能夠先聲明,而不給初值,這中變量也稱爲final空白,不管什麼狀況,編譯器都確保空白final在使用以前必須被初始化。
可是,final空白在final關鍵字final的使用上提供了更大的靈活性,爲此,一個類中的final數據成員就能夠實現依對象而有所不一樣,卻有保持其恆定不變的特徵。
public class FinalTest { final int p; final int q=3; FinalTest(){ p=1; } FinalTest(int i){ p=i;//能夠賦值,至關於直接定義p q=i;//不能爲一個final變量賦值 } }
剛提到了內嵌機制,如今詳細展開。
要知道調用一個函數除了函數自己的執行時間以外,還須要額外的時間去尋找這個函數(類內部有一個函數簽名和函數地址的映射表)。因此減小函數調用次數就等於下降了性能消耗。
final修飾的函數會被編譯器優化,優化的結果是減小了函數調用的次數。如何實現的,舉個例子給你看:
public class Test{ final void func(){System.out.println("g");}; public void main(String[] args){ for(int j=0;j<1000;j++) func(); }} 通過編譯器優化以後,這個類變成了至關於這樣寫: public class Test{ final void func(){System.out.println("g");}; public void main(String[] args){ for(int j=0;j<1000;j++) {System.out.println("g");} }}
看出來區別了吧?編譯器直接將func的函數體內嵌到了調用函數的地方,這樣的結果是節省了1000次函數調用,固然編譯器處理成字節碼,只是咱們能夠想象成這樣,看個明白。
不過,當函數體太長的話,用final可能拔苗助長,由於通過編譯器內嵌以後代碼長度大大增長,因而就增長了jvm解釋字節碼的時間。
在使用final修飾方法的時候,編譯器會將被final修飾過的方法插入到調用者代碼處,提升運行速度和效率,但被final修飾的方法體不能過大,編譯器可能會放棄內聯,但究竟多大的方法會放棄,我尚未作測試來計算過。
下面這些內容是經過兩個疑問來繼續闡述的
見下面的測試代碼,我會執行五次:
public class Test { public static void getJava() { String str1 = "Java "; String str2 = "final "; for (int i = 0; i < 10000; i++) { str1 += str2; } } public static final void getJava_Final() { String str1 = "Java "; String str2 = "final "; for (int i = 0; i < 10000; i++) { str1 += str2; } } public static void main(String[] args) { long start = System.currentTimeMillis(); getJava(); System.out.println("調用不帶final修飾的方法執行時間爲:" + (System.currentTimeMillis() - start) + "毫秒時間"); start = System.currentTimeMillis(); String str1 = "Java "; String str2 = "final "; for (int i = 0; i < 10000; i++) { str1 += str2; } System.out.println("正常的執行時間爲:" + (System.currentTimeMillis() - start) + "毫秒時間"); start = System.currentTimeMillis(); getJava_Final(); System.out.println("調用final修飾的方法執行時間爲:" + (System.currentTimeMillis() - start) + "毫秒時間"); } }
結果爲: 第一次: 調用不帶final修飾的方法執行時間爲:1732毫秒時間 正常的執行時間爲:1498毫秒時間 調用final修飾的方法執行時間爲:1593毫秒時間 第二次: 調用不帶final修飾的方法執行時間爲:1217毫秒時間 正常的執行時間爲:1031毫秒時間 調用final修飾的方法執行時間爲:1124毫秒時間 第三次: 調用不帶final修飾的方法執行時間爲:1154毫秒時間 正常的執行時間爲:1140毫秒時間 調用final修飾的方法執行時間爲:1202毫秒時間 第四次: 調用不帶final修飾的方法執行時間爲:1139毫秒時間 正常的執行時間爲:999毫秒時間 調用final修飾的方法執行時間爲:1092毫秒時間 第五次: 調用不帶final修飾的方法執行時間爲:1186毫秒時間 正常的執行時間爲:1030毫秒時間 調用final修飾的方法執行時間爲:1109毫秒時間 由以上運行結果不難看出,執行最快的是「正常的執行」即代碼直接編寫,而使用final修飾的方法,不像有些書上或者文章上所說的那樣,速度與效率與「正常的執行」無異,而是位於第二位,最差的是調用不加final修飾的方法。
觀點:加了比不加好一點。
見代碼:
public class Final { public static void main(String[] args) { Color.color[3] = "white"; for (String color : Color.color) System.out.print(color+" "); } } class Color { public static final String[] color = { "red", "blue", "yellow", "black" }; }
執行結果: red blue yellow white 看!,黑色變成了白色。
在使用findbugs插件時,就會提示public static String[] color = { "red", "blue", "yellow", "black" };這行代碼不安全,但加上final修飾,這行代碼仍然是不安全的,由於final沒有作到保證變量的值不會被修改!
緣由是:final關鍵字只能保證變量自己不能被賦與新值,而不能保證變量的內部結構不被修改。例如在main方法有以下代碼Color.color = new String[]{""};就會報錯了。
那可能有的同窗就會問了,加上final關鍵字不能保證數組不會被外部修改,那有什麼方法可以保證呢?答案就是下降訪問級別,把數組設爲private。這樣的話,就解決了數組在外部被修改的不安全性,但也產生了另外一個問題,那就是這個數組要被外部使用的。
解決這個問題見代碼:
import java.util.AbstractList; import java.util.List; public class Final { public static void main(String[] args) { for (String color : Color.color) System.out.print(color + " "); Color.color.set(3, "white"); } } class Color { private static String[] _color = { "red", "blue", "yellow", "black" }; public static List<String> color = new AbstractList<String>() { @Override public String get(int index) { return _color[index]; } @Override public String set(int index, String value) { throw new RuntimeException("爲了代碼安全,不能修改數組"); } @Override public int size() { return _color.length; } };
}
這樣就OK了,既保證了代碼安全,又能讓數組中的元素被訪問了。
規則1:final修飾的方法不能夠被重寫。
規則2:final修飾的方法僅僅是不能重寫,但它徹底能夠被重載。
規則3:父類中private final方法,子類能夠從新定義,這種狀況不是重寫。
代碼示例
規則1代碼 public class FinalMethodTest { public final void test(){} } class Sub extends FinalMethodTest { // 下面方法定義將出現編譯錯誤,不能重寫final方法 public void test(){} } 規則2代碼 public class Finaloverload { //final 修飾的方法只是不能重寫,徹底能夠重載 public final void test(){} public final void test(String arg){} } 規則3代碼 public class PrivateFinalMethodTest { private final void test(){} } class Sub extends PrivateFinalMethodTest { // 下面方法定義將不會出現問題 public void test(){} }
與前面介紹的鎖和 volatile 相比較,對 final 域的讀和寫更像是普通的變量訪問。對於 final 域,編譯器和處理器要遵照兩個重排序規則:
下面,咱們經過一些示例性的代碼來分別說明這兩個規則:
<pre>public class FinalExample {
int i; // 普通變量 final int j; //final 變量 static FinalExample obj;
public void FinalExample () { // 構造函數 i = 1; // 寫普通域 j = 2; // 寫 final 域 } public static void writer () { // 寫線程 A 執行 obj = new FinalExample (); } public static void reader () { // 讀線程 B 執行 FinalExample object = obj; // 讀對象引用 int a = object.i; // 讀普通域 int b = object.j; // 讀 final 域 }
}
</pre>
這裏假設一個線程 A 執行 writer () 方法,隨後另外一個線程 B 執行 reader () 方法。下面咱們經過這兩個線程的交互來講明這兩個規則。
寫 final 域的重排序規則禁止把 final 域的寫重排序到構造函數以外。這個規則的實現包含下面 2 個方面:
如今讓咱們分析 writer () 方法。writer () 方法只包含一行代碼:finalExample = new FinalExample ()。這行代碼包含兩個步驟:
假設線程 B 讀對象引用與讀對象的成員域之間沒有重排序(立刻會說明爲何須要這個假設),下圖是一種可能的執行時序:
在上圖中,寫普通域的操做被編譯器重排序到了構造函數以外,讀線程 B 錯誤的讀取了普通變量 i 初始化以前的值。而寫 final 域的操做,被寫 final 域的重排序規則「限定」在了構造函數以內,讀線程 B 正確的讀取了 final 變量初始化以後的值。
寫 final 域的重排序規則能夠確保:在對象引用爲任意線程可見以前,對象的 final 域已經被正確初始化過了,而普通域不具備這個保障。以上圖爲例,在讀線程 B「看到」對象引用 obj 時,極可能 obj 對象尚未構造完成(對普通域 i 的寫操做被重排序到構造函數外,此時初始值 2 尚未寫入普通域 i)。
讀 final 域的重排序規則以下:
初次讀對象引用與初次讀該對象包含的 final 域,這兩個操做之間存在間接依賴關係。因爲編譯器遵照間接依賴關係,所以編譯器不會重排序這兩個操做。大多數處理器也會遵照間接依賴,大多數處理器也不會重排序這兩個操做。但有少數處理器容許對存在間接依賴關係的操做作重排序(好比 alpha 處理器),這個規則就是專門用來針對這種處理器。
reader() 方法包含三個操做:
如今咱們假設寫線程 A 沒有發生任何重排序,同時程序在不遵照間接依賴的處理器上執行,下面是一種可能的執行時序:
在上圖中,讀對象的普通域的操做被處理器重排序到讀對象引用以前。讀普通域時,該域尚未被寫線程 A 寫入,這是一個錯誤的讀取操做。而讀 final 域的重排序規則會把讀對象 final 域的操做「限定」在讀對象引用以後,此時該 final 域已經被 A 線程初始化過了,這是一個正確的讀取操做。
讀 final 域的重排序規則能夠確保:在讀一個對象的 final 域以前,必定會先讀包含這個 final 域的對象的引用。在這個示例程序中,若是該引用不爲 null,那麼引用對象的 final 域必定已經被 A 線程初始化過了。
上面咱們看到的 final 域是基礎數據類型,下面讓咱們看看若是 final 域是引用類型,將會有什麼效果?
請看下列示例代碼:
<pre>public class FinalReferenceExample {
final int[] intArray; //final 是引用類型
static FinalReferenceExample obj;
public FinalReferenceExample () { // 構造函數
intArray = new int[1]; //1 intArray[0] = 1; //2
}
public static void writerOne () { // 寫線程 A 執行
obj = new FinalReferenceExample (); //3
}
public static void writerTwo () { // 寫線程 B 執行
obj.intArray[0] = 2; //4
}
public static void reader () { // 讀線程 C 執行
if (obj != null) { //5 int temp1 = obj.intArray[0]; //6 }
}
}
</pre>
這裏 final 域爲一個引用類型,它引用一個 int 型的數組對象。對於引用類型,寫 final 域的重排序規則對編譯器和處理器增長了以下約束:
對上面的示例程序,咱們假設首先線程 A 執行 writerOne() 方法,執行完後線程 B 執行 writerTwo() 方法,執行完後線程 C 執行 reader () 方法。下面是一種可能的線程執行時序:
在上圖中,1 是對 final 域的寫入,2 是對這個 final 域引用的對象的成員域的寫入,3 是把被構造的對象的引用賦值給某個引用變量。這裏除了前面提到的 1 不能和 3 重排序外,2 和 3 也不能重排序。
JMM 能夠確保讀線程 C 至少能看到寫線程 A 在構造函數中對 final 引用對象的成員域的寫入。即 C 至少能看到數組下標 0 的值爲 1。而寫線程 B 對數組元素的寫入,讀線程 C 可能看的到,也可能看不到。JMM 不保證線程 B 的寫入對讀線程 C 可見,由於寫線程 B 和讀線程 C 之間存在數據競爭,此時的執行結果不可預知。
若是想要確保讀線程 C 看到寫線程 B 對數組元素的寫入,寫線程 B 和讀線程 C 之間須要使用同步原語(lock 或 volatile)來確保內存可見性。
https://www.infoq.cn/article/...
https://www.jianshu.com/p/067...
https://www.jianshu.com/p/f68...
https://www.cnblogs.com/xiaox...
https://www.iteye.com/blog/ca...
https://blog.csdn.net/chengqi...
https://blog.csdn.net/hupuxia...
若是你們想要實時關注我更新的文章以及分享的乾貨的話,能夠關注個人公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站,做者黃小斜,專一 Java 相關技術:SSM、SpringBoot、MySQL、分佈式、中間件、集羣、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術乾貨和學習經驗,致力於Java全棧開發!
Java工程師必備學習資源: 一些Java工程師經常使用學習資源,關注公衆號後,後臺回覆關鍵字 「Java」 便可免費無套路獲取。
做者是 985 碩士,螞蟻金服 JAVA 工程師,專一於 JAVA 後端技術棧:SpringBoot、MySQL、分佈式、中間件、微服務,同時也懂點投資理財,偶爾講點算法和計算機理論基礎,堅持學習和寫做,相信終身學習的力量!
程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公衆號後,後臺回覆關鍵字 「資料」 便可免費無套路獲取。