一、final
(一)、final的使用
final關鍵字可以用來修飾類、方法和變量(包括成員變量和局部變量)
1. 當用final修飾一個類時,表明這個類不能被繼承。
2. 當用final修飾一個方法時,表明這個方法不能被重寫。
3. 當用final修飾一個變量時,表明這個變量初始化后就不能再被修改。
final可以理解為"最后的、最終的"。與類而言,是不能被繼承;與方法而言,是不能被覆蓋;與變量而言,是不能再修改。
使用final修飾類的原因:
1. 設計原因(Design)
從設計的角度來考慮為final類,此時final 的語義表明為:這個類不想在關系結構上做出任何的改變,也不希望有任何人可以繼承自這個類,除此之外,就沒有更多的限制了。
2. 效率原因(Efficiency)
這里涉及到內聯機制。一個類被final修飾后,它的方法默認被修飾為final ,這時方法的內聯起到作用了。會將所有對方法的調用轉化為inline調用的機制,大大提高執行效率。
final的實現原理:
1. final修飾域(基本數據類型、引用類型)
在一個類中被final修飾的域會在編譯時放入常量池。
在編譯后得到的.class文件中,有這么一塊內容,叫常量池。常量池中的確包含了常量,當然還有其他的內容。一個類中被final修飾的域在這個時候就會被放入這個大池子中。
至於為什么這么做?原因很簡單,為了效率。 其實將一個基本數據類型修飾為final的目的最單純最美好,就是希望它不要變。這樣系統有就可以做一些優化操作,將這些常量值裝在需要計算的過程中,讓它們充當類似於宏的身份,換句話說,編譯器可以在編譯期間提前完成一些計算工作,省去了在運行時對於變量的相對復雜的操作。那么到這里就完成了么?其實不是的,這里要補充的一點就是一個編譯期間的類文件中,常量池中的基本數據類型的常量是不知道具體的值是什么,換句話說,在文件編譯過后,雖然知道一個域是常量,但是至於這個常量的具體內容是什么,此時是無從知曉的。
只有當運行時,常量才會真正的被賦值,對於static和沒有static修飾的基本數據類型來說,是有差異的,差異就在於static修飾的域是在類載入的時候進行初始化的,所有實例共享同一個常量,同時Java虛擬機沒有把它當作類變量,在使用它的任何類的常量池或者字節碼流中直接存放的是它表示的常量值。
2. final修飾類和方法
Java的沙箱為了保證裝載的類文件的安全性,會在驗證階段對字節碼流做多次的驗證,那么其中就包括對各個類之間的二進制兼容的檢查,其中就包括,
1. 檢查final的類不能擁有子類
2. 檢查final的方法不能被覆蓋
(二)、final域的內存語義
final域是基礎數據類型時的重排序規則:
寫final域的重排序規則:在構造函數內對一個final域的寫入,與隨后把這個被構造對象的引用賦值給一個引用對象,這個兩個操作之間不能重排序。(禁止把final域的寫重排序到構造函數之外)
寫final域的重排序規則可以確保:在對象引用為任意線程可見之前,對象的final域已經被正確初始化過了,而普通域不具有這個保障。
讀final域的重排序規則:初次讀一個包含final域的對象的引用,與隨后初次讀這個對象包含的final域,這兩個操作之間不能重排序。(在一個線程中,初次讀對象引用,與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作)
讀final域的重排序規則可以確保:在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用。
final域是引用類型時的重排序規則:(相比final域是基礎數據類型的情況,增加了以下約束)
寫final域的重排序規則:在構造函數內對一個final引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。
為什么final引用不能沖構造函數內“逸出”(可以看出寫final域的重排序規則保證了final域的寫入被限定在構造函數內執行)
在構造函數返回前,被構造對象的引用還不能為其他線程所見,因為此時的final域可能還沒有被初始化。
參考:《Java並發編程的藝術》
final類型的變量可以保證在多線程發布某個對象時,這個對象的final域變量能夠被正常的初始化(在寫final變量后加了storestore屏障,在讀final變量前加了loadload屏障),而普通類型的變量可能不會被正確的初始化,這樣導致該對象在多個線程之間出現不一致的情況,這也就是我們所說的引用溢出。
二、static
(一)、static的使用(可以在沒有創建任何對象的前提下,僅僅通過類本身來調用static方法)
static可以用來修飾類的成員方法、類的成員變量,另外可以編寫static代碼塊來優化程序性能。
1. static方法,static方法一般稱為靜態方法,靜態方法不依賴於任何對象就可以進行訪問
2. static變量,static變量也稱作靜態變量,靜態變量和非靜態變量的區別是:靜態變量被所有的對象所共享,在內存中只有一個副本,它當且僅當在類初次加載時會被初始化。而非靜態變量是對象所擁有的,在創建對象的時候被初始化,存在多個副本,各個對象擁有的副本互不影響。
static成員變量的初始化順序按照定義的順序進行初始化。
3. static代碼塊,static塊可以置於類中的任何地方,類中可以有多個static塊。在類初次被加載的時候,會按照static塊的順序來執行每個static塊,並且只會執行一次。(因為只在類加載的時候執行一次,因此可以用static代碼塊來優化程序性能)
另外這里還有一個例子
class Parent { static String name = "hello"; //非靜態代碼塊 { System.out.println("parent block"); } static { System.out.println("parent static block"); } public Parent(String name) { System.out.println("name"); System.out.println("parent constructor"); } } class Child extends Parent { static String childName = "hello"; { System.out.println("child block"); } static { System.out.println("child static block"); } public Child() { super("name"); System.out.println("child constructor"); } } public class TestStatic { public static void main(String[] args) { new Child();// 語句(*) } }
運行結果:
parent static block child static block parent block parent constructor child block child constructor
分析:當執行new Child()時,它首先去看父類里面有沒有靜態代碼塊,如果有,它先去執行父類里面靜態代碼塊里面的內容,當父類的靜態代碼塊里面的內容執行完畢之后,接着去執行子類(自己這個類)里面的靜態代碼塊,當子類的靜態代碼塊執行完畢之后,它接着又去看父類有沒有非靜態代碼塊,如果有就執行父類的非靜態代碼塊,父類的非靜態代碼塊執行完畢,接着執行父類的構造方法;父類的構造方法執行完畢之后,它接着去看子類有沒有非靜態代碼塊,如果有就執行子類的非靜態代碼塊。子類的非靜態代碼塊執行完畢再去執行子類的構造方法,這個就是一個對象的初始化順序。
總結:
對象的初始化順序:首先執行父類靜態的內容,父類靜態的內容執行完畢后,接着去執行子類的靜態的內容,當子類的靜態內容執行完畢之后,再去看父類有沒有非靜態代碼塊,如果有就執行父類的非靜態代碼塊,父類的非靜態代碼塊執行完畢,接着執行父類的構造方法;父類的構造方法執行完畢之后,它接着去看子類有沒有非靜態代碼塊,如果有就執行子類的非靜態代碼塊。子類的非靜態代碼塊執行完畢再去執行子類的構造方法。總之一句話,靜態代碼塊內容先執行,接着執行父類非靜態代碼塊和構造方法,然后執行子類非靜態代碼塊和構造方法。
注意:子類的構造方法,不管這個構造方法帶不帶參數,默認的它都會先去尋找父類的不帶參數的構造方法。如果父類沒有不帶參數的構造方法,那么子類必須用supper關鍵子來調用父類帶參數的構造方法,否則編譯不能通過。
三、final和static在一起使用(他們同時使用時既可修飾成員變量,也可修飾成員方法。)
1. 對於成員變量,該變量一旦賦值就不能改變,該變量被類的所有實例共享,我們稱它為“全局常量”。可以通過類名直接訪問。
2. 對於成員方法,表示該方法不可繼承和改變。可以通過類名直接訪問。