您好,登錄后才能下訂單哦!
本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫里查看
https://github.com/h3pl/Java-Tutorial
喜歡的話麻煩點下Star哈
文章首發于我的個人博客:
www.how2playlife.com
本文是微信公眾號【Java技術江湖】的《夯實Java基礎系列博文》其中一篇,本文部分內容來源于網絡,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術博客內容,引用其中了一些比較好的博客文章,如有侵權,請聯系作者。
該系列博文會告訴你如何從入門到進階,一步步地學習Java基礎知識,并上手進行實戰,接著了解每個Java知識點背后的實現原理,更完整地了解整個Java技術體系,形成自己的知識框架。為了更好地總結和檢驗你的學習成果,本系列文章也會提供部分知識點對應的面試題以及參考答案。
如果對本系列文章有什么建議,或者是有什么疑問的話,也可以關注公眾號【Java技術江湖】聯系作者,歡迎你參與本系列博文的創作和修訂。
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方法的好處:
1、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 域,編譯器和處理器要遵守兩個重排序規則:
下面,我們通過一些示例性的代碼來分別說明這兩個規則:
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 域 } }
這里假設一個線程 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 域是引用類型,將會有什么效果?
請看下列示例代碼:
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 } } }
這里 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/java-memory-model-6
https://www.jianshu.com/p/067b6c89875a
https://www.jianshu.com/p/f68d6ef2dcf0
https://www.cnblogs.com/xiaoxi/p/6392154.html
https://www.iteye.com/blog/cakin24-2334965
https://blog.csdn.net/chengqiuming/article/details/70139503
https://blog.csdn.net/hupuxiang/article/details/7362267
黃小斜是跨考軟件工程的 985 碩士,自學 Java 兩年,拿到了 BAT 等近十家大廠 offer,從技術小白成長為阿里工程師。
作者專注于 JAVA 后端技術棧,熱衷于分享程序員干貨、學習經驗、求職心得和程序人生,目前黃小斜的CSDN博客有百萬+訪問量,知乎粉絲2W+,全網已有10W+讀者。
黃小斜是一個斜杠青年,堅持學習和寫作,相信終身學習的力量,希望和更多的程序員交朋友,一起進步和成長!
原創電子書:
關注微信公眾號【黃小斜】后回復【原創電子書】即可領取我原創的電子書《菜鳥程序員修煉手冊:從技術小白到阿里巴巴Java工程師》這份電子書總結了我2年的Java學習之路,包括學習方法、技術總結、求職經驗和面試技巧等內容,已經幫助很多的程序員拿到了心儀的offer!
程序員3T技術學習資源: 一些程序員學習技術的資源大禮包,關注公眾號后,后臺回復關鍵字 “資料” 即可免費無套路獲取,包括Java、python、C++、大數據、機器學習、前端、移動端等方向的技術資料。
如果大家想要實時關注我更新的文章以及分享的干貨的話,可以關注我的微信公眾號【Java技術江湖】
這是一位阿里 Java 工程師的技術小站。作者黃小斜,專注 Java 相關技術:SSM、SpringBoot、MySQL、分布式、中間件、集群、Linux、網絡、多線程,偶爾講點Docker、ELK,同時也分享技術干貨和學習經驗,致力于Java全棧開發!
(關注公眾號后回復”Java“即可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分布式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程序員面試指南等干貨資源)
Java工程師必備學習資源: 一些Java工程師常用學習資源,關注公眾號后,后臺回復關鍵字 “Java” 即可免費無套路獲取。
?
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。