Java final關鍵字及其內存語義

final是Java中的一個關鍵字,final可用於修飾類、方法、參數和變量(包括實例變量和類變量)。bash

final修飾類

final修飾的類具備不可繼承性,也就是若是一個類是final類型的,則這個類不容許有子類。首先咱們頂一個final類:ide

public final class FinalClass {
    private int field;
}
複製代碼

而後若是咱們嘗試去繼承這個類的話編譯器會報錯:函數

public class FalseExtension extends FinalClass {

}
複製代碼
FalseExtension
編譯器提示咱們:cannot extend final class,也就是說final類型的類不容許繼承。

final修飾方法

final修飾的方法具備不可變性,也就是說final的方法不容許在子類中被覆寫(@Override)。下面咱們來看一個反例:優化

finalMethod
在Base子類Extension中咱們覆寫(Override)了子類中的final方法,編譯器提示錯誤:final方法不能被覆寫。

final修飾參數和變量

若是參數用final修飾,那麼在方法中咱們不能對這個final參數進行修改:ui

public void test(final int x) {
    // x++; // 這句是非法的,由於x是final的
}
複製代碼

final修飾的變量(包括實例變量和類變量)具備引用不可變性,例如:private final int x = 1,這裏變量x是final類型的,若是咱們嘗試修改x:x = 2,編譯器就會提示出錯。 一樣的對於包裝類,若是咱們嘗試修改變量的指針,同樣會提示錯誤:this

class Test {
    
    private int x;

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }
}

final Test test = new Test();
// test = new Test(); // 這個表達式是非法的,由於test是final的
test.setX(2); // 這個表達式是合法的由於這個表達式沒有修改test的引用
複製代碼

注意這裏包裝類的不可變性是指引用的不可變,若是咱們不修改變量的引用,而是經過訪問變量所指向的包裝類的方法去修改包裝類的屬性,這個是合法的。spa

編譯器對final變量的優化

請看下面的例子:線程

public class Test {
    public static void main(String[] args)  {
        String a = "helloworld1"; 
        final String b = "helloworld";
        String c = "helloworld";
        String d = b + 2; 
        String e = d + 2;
        System.out.println((a == d));
        System.out.println((a == e));
    }
}
複製代碼

輸出結果:指針

true
false
複製代碼

能夠看到a和d是指向同一個地址,而a和e則是指向不一樣的地址。這裏就能夠看出final變量和普通變量的區別了。變量b是final類型的,編譯器知道b的引用不會改變,所以直接能夠「算出來」d就是「helloworld2」,那麼a和d都是指向"helloworld2"字面量的變量,天然a == d成立了。編譯器知道一個字符串變量是final不可變的變量後,就能夠直接進行替換了。對於編譯期間不能肯定的final變量,編譯器則不會進行替換,請看下面這個例子:code

public class Test {
    public static void main(String[] args)  {
        String a = "helloworld2"; 
        final String b = getHelloWorld();
        String c = b + 2; 
        System.out.println((a == c));
 
    }
     
    public static String getHelloWorld() {
        return "helloworld";
    }
}
複製代碼

這段代碼的輸出結果爲false。

由於編譯器沒法在編譯期就能肯定b爲"helloworld",所以編譯器沒法對b進行替換,因此a和c是不等的。

final的內存語義

final域的重排序規則

  1. 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
  2. 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。

寫final域的重排序規則

寫final域的重排序規則禁止把final域的寫重排序到構造函數以外。這個規則的實現包含下面2個方面:

  1. JMM禁止編譯器把final域的寫重排序到構造函數以外。
  2. 編譯器會在final域的寫以後,構造函數return以前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數以外。寫final域的重排序規則能夠確保:在對象引用爲任意線程可見以前,對象的final域已經被正確初始化過了,而普通域不具備這個保障。

讀final域的重排序規則

讀final域的重排序規則是,在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操做的前面插入一個LoadLoad屏障。初次讀對象引用與初次讀該對象包含的final域,這兩個操做之間存在間接依賴關係。因爲編譯器遵照間接依賴關係,所以編譯器不會重排序這兩個操做。大多數處理器也會遵照間接依賴,也不會重排序這兩個操做。但有少數處理器容許對存在間接依賴關係的操做作重排序(好比alpha處理器),這個規則就是專門用來針對這種處理器的。讀final域的重排序規則能夠確保:在讀一個對象的final域以前,必定會先讀包含這個final域的對象的引用。

final域爲引用類型

對於引用類型,寫final域的重排序規則對編譯器和處理器增長了以下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。這一規則確保了其餘線程能讀到被正確初始化的final引用對象的成員域。

相關文章
相關標籤/搜索