final關鍵字深入解析

你不要我扔垃圾桶了哦發表於2019-03-02

final關鍵字特性

final關鍵字在java中使用非常廣泛,可以申明成員變數、方法、類、本地變數。一旦將引用宣告為final,將無法再改變這個引用。final關鍵字還能保證記憶體同步,本部落格將會從final關鍵字的特性到從java記憶體層面保證同步講解。這個內容在面試中也有可能會出現。

final使用

final變數

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);
    }
}
複製程式碼

final方法

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類中的方法預設也會是final型別的,java中的String類和Integer類都是final型別的。

final class PersonalLoan{}
 
class CheapPersonalLoan extends PersonalLoan {  //編譯錯誤,無法被繼承 
}
複製程式碼

final關鍵字的知識點

  1. final成員變數必須在宣告的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。final變數一旦被初始化後不能再次賦值。
  2. 本地變數必須在宣告時賦值。 因為沒有初始化的過程
  3. 在匿名類中所有變數都必須是final變數。
  4. final方法不能被重寫, final類不能被繼承
  5. 介面中宣告的所有變數本身是final的。類似於匿名類
  6. final和abstract這兩個關鍵字是反相關的,final類就不可能是abstract的。
  7. final方法在編譯階段繫結,稱為靜態繫結(static binding)。
  8. 將類、方法、變數宣告為final能夠提高效能,這樣JVM就有機會進行估計,然後優化。

final方法的好處:

  1. 提高了效能,JVM在常量池中會快取final變數
  2. final變數在多執行緒中併發安全,無需額外的同步開銷
  3. final方法是靜態編譯的,提高了呼叫速度
  4. final類建立的物件是隻可讀的,在多執行緒可以安全共享

從java記憶體模型中理解final關鍵字

java記憶體模型對final域遵守如下兩個重拍序規則

  1. 初次讀一個包含final域的物件的引用和隨後初次寫這個final域,不能重拍序。
  2. 在建構函式內對final域寫入,隨後將建構函式的引用賦值給一個引用變數,操作不能重排序。

以上兩個規則就限制了final域的初始化必須在建構函式內,不能重拍序到建構函式之外,普通變數可以。

具體的操作是

  1. java記憶體模型在final域寫入和建構函式返回之前,插入一個StoreStore記憶體屏障,靜止處理器將final域重拍序到建構函式之外。
  2. java記憶體模型在初次讀final域的物件和讀物件內final域之間插入一個LoadLoad記憶體屏障。

new一個物件至少有以下3個步驟

  1. 在堆中申請一塊記憶體空間
  2. 物件進行初始化
  3. 將記憶體空間的引用賦值給一個引用變數,可以理解為呼叫invokespecial指令

普通成員變數在初始化時可以重排序為1-3-2,即被重拍序到建構函式之外去了。 final變數在初始化必須為1-2-3。

讀寫final域重拍序規則

public class FinalExample {
    int i;               
    final int j;
    static FinalExample obj;

    public void FinalExample () {
        i = 1;                   // 1
        j = 2;                   // 2
    }

    public static void writer () {  //寫執行緒A  
        obj = new FinalExample ();  // 3
    }

    public static void reader () {       //讀執行緒B執行
        if(obj != null) {               //4
            int a = object.i;           //5
            int b = object.j;           //6
        }
    }
}
複製程式碼

我們可以用happens-before來分析可見性。結果是保證a讀取到的值可能為0,或者1 而b讀取的值一定為2。
首先,由final的重拍序規則決定3HB2,但是3和1不存在HB關係,原因在上面說過了。 因為執行緒B線上程A之後執行,所以3HB4。
那麼2和4的HB關係怎麼確定?? final的重拍序規則規定final的賦值必須在建構函式的return之前。所以2HB4。因為在一個執行緒內4HB6.所以可以得出結論2HB5。則b一定能得到j的最新值。而a就不一定了,因為沒有HB關係,可以讀到任意值。

HB判斷可見性關係真是太方便了。可以參考我的另外一個部落格http://medesqure.top/2018/08/25/happen-before/

可能發生的執行時序如下所示。

final關鍵字深入解析

final物件是引用型別

如果final域是一個引用型別,比如引用的是一個int型別的陣列。對於引用型別,寫final域的重拍序規則增加了如下的約束

  1. 在建構函式內對一個final引用的物件的成員域的寫入和隨後在建構函式外將被構造物件的引用賦值給引用變數之間不能重拍序。 即先寫int[]陣列的內容,再將引用丟擲去。
public class FinalReferenceExample {
    final int[] intArray;                     //final是引用型別
    static FinalReferenceExample obj;
    
    public FinalReferenceExample () {        //建構函式  在建構函式中不能被重排序 final型別在宣告或者在建構函式中要賦值。
        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
        }
    }
}
複製程式碼

JMM保證了3和2之間的有序性。同樣可以使用HB原則去分析,這裡就不分析了。執行順序如下所示。

6DBA7734-EFF8-4AC2-8E3B-E1645889A109

final引用不能從建構函式“逸出”

JMM對final域的重拍序規則保證了能安全讀取final域時已經在建構函式中被正確的初始化了。
但是如果在建構函式內將被建構函式的引用為其他執行緒可見,那麼久存在物件引用在建構函式中逸出,final的可見性就不能保證。 其實理解起來很簡單,就是在其他執行緒的角度去觀察另一個執行緒的指令其實是重拍序的。

public class FinalReferenceEscapeExample {
    final int i;
    static FinalReferenceEscapeExample obj;
    
    public FinalReferenceEscapeExample () {
        i = 1;       //1寫final域
        obj = this;  //2 this引用在此“逸出”  因為obj不是final型別的,所以不用遵守可見性  }
    
    public static void writer() {
        new FinalReferenceEscapeExample ();
    }

    public static void reader {
        if (obj != null) {                     //3
            int temp = obj.i;                 //4
        }
    }
}
複製程式碼

操作1的和操作2可能被重拍序。在其他執行緒觀察時就會訪問到未被初始化的變數i,可能的執行順序如圖所示。

AAF34760-7112-463C-852F-25CB775AFD62

本文結束,歡迎閱讀。
本人部落格 medesqure.top/ 歡迎觀看

相關文章