Java面試題,深刻理解final關鍵字

final關鍵字

final的簡介

final能夠修飾變量,方法和類,用於表示所修飾的內容一旦賦值以後就不會再被改變,好比String類就是一個final類型的類。java

final的具體使用場景

final可以修飾變量,方法和類,也就是final使用範圍基本涵蓋了java每一個地方, 下面就分別以鎖修飾的位置:變量,方法和類分別介紹。程序員

final修飾成員變量

public class FinalExample {    //聲明變量的時候,就進行初始化
    private final int num=6;    //類變量必需要在靜態初始化塊中指定初始值或者聲明該類變量時指定初始值
    // private final String str; //編譯錯誤:由於非靜態變量不能夠在靜態初始化快中賦初值
    private final static String name;    private final double score;    private final char ch;    //private final char ch2;//編譯錯誤:TODO:由於沒有在構造器、初始化代碼塊和聲明時賦值
    
    {        //實例變量在初始化代碼塊賦初值
        ch='a';
    }    
    static {
        name="aaaaa";
    }    
    public FinalExample(){        //num=1;編譯錯誤:已經賦值後,就不能再修改了
        score=90.0;
    }    
    public void ch2(){        //ch2='c';//編譯錯誤:實例方法沒法給final變量賦值
    }
}複製代碼
  • 類變量:必需要在靜態初始化塊中指定初始值或者聲明該類變量時指定初始值,並且只能在這兩個地方之一進行指定數組

  • 實例變量:必要要在非靜態初始化塊,聲明該實例變量或者在構造器中指定初始值,並且只能在這三個地方進行指定安全

final修飾局部變量

final局部變量由程序員進行顯式初始化, 若是final局部變量已經進行了初始化則後面就不能再次進行更改, 若是final變量未進行初始化,能夠進行賦值,當且僅有一次賦值,一旦賦值以後再次賦值就會出錯。bash

public void test(){    final int a=1;    //a=2;//編譯錯誤:final局部變量已經進行了初始化則後面就不能再次進行更改}複製代碼

final基本數據類型 VS final引用數據類型app

若是final修飾的是一個基本數據類型的數據,一旦賦值後就不能再次更改, 那麼,若是final是引用數據類型了?這個引用的對象可以改變嗎?ide

public class FinalExample2 {    private static class Person {        private String name;        private int age;        public void setName(String name) {            this.name = name;
        }        public String getName() {            return name;
        }        public void setAge(int age) {            this.age = age;
        }        public int getAge() {            return age;
        }        public Person(String name, int age) {            this.name=name;            this.age = age;
        }        @Override
        public String toString() {            StringBuilder res=new StringBuilder();
            res.append("[").append("name="+name+",age="+age).append("]");            return res.toString();
        }
    }    private static final Person person=new Person("小李子",23);    public static void main(String[] args) {        System.out.println(person);
        person.setAge(24);        System.out.println(person);
    }
}複製代碼

輸出結果:函數

[name=小李子,age=23]
[name=小李子,age=24]複製代碼

當咱們對final修飾的引用數據類型變量person的屬性改爲24,是能夠成功操做的。 經過這個實驗咱們就能夠看出來當final修飾基本數據類型變量時,不能對基本數據類型變量從新賦值, 所以基本數據類型變量不能被改變。 而對於引用類型變量而言,它僅僅保存的是一個引用,final只保證這個引用類型變量所引用的地址不會發生改變, 即一直引用這個對象,但這個對象屬性是能夠改變的。ui

宏變量this

利用final變量的不可更改性,在知足如下三個條件時,該變量就會成爲一個「宏變量」,便是一個常量。

  • 使用final修飾符修飾

  • 在定義該final變量時就指定了初始值;

  • 該初始值在編譯時就可以惟一肯定

注意:當程序中其餘地方使用該宏變量的地方,編譯器會直接替換成該變量的值。

final修飾方法

重寫(Override)

被final修飾的方法不可以被子類所重寫。 好比在Object中,getClass()方法就是final的,咱們就不能重寫該方法, 可是hashCode()方法就不是被final所修飾的,咱們就能夠重寫hashCode()方法。

重載(Overload)

被final修飾的方法是能夠重載的

final修飾類

當一個類被final修飾時,該類是不能被子類繼承的。 子類繼承每每能夠重寫父類的方法和改變父類屬性,會帶來必定的安全隱患, 所以,當一個類不但願被繼承時就可使用final修飾。

不可變類

final常常會被用做不變類上。咱們先來看看什麼是不可變類:

  • 使用private和final修飾符來修飾該類的成員變量

  • 提供帶參的構造器用於初始化類的成員變量

  • 僅爲該類的成員變量提供getter方法,不提供setter方法,由於普通方法沒法修改fina修飾的成員變量

  • 若是有必要就重寫Object類的hashCode()和equals()方法,應該保證用equals()判斷相同的兩個對象其Hashcode值也是相等的。

JDK中提供的八個包裝類和String類都是不可變類。

final域重排序規則

final爲基本類型

public class FinalDemo {    private int a;  //普通域
    private final int b; //final域-->int基本類型
    private static FinalDemo finalDemo;//引用類型,但不是final修飾的

    public FinalDemo() {
        a = 1; // 1. 寫普通域
        b = 2; // 2. 寫final域
    }    public static void writer() {
        finalDemo = new FinalDemo();
    }    public static void reader() {        FinalDemo demo = finalDemo; // 3.讀對象引用
        int a = demo.a;    //4.讀普通域
        int b = demo.b;    //5.讀final域
    }
}複製代碼

假設線程A在執行writer()方法,線程B執行reader()方法。

寫final域重排序規則

寫final域的重排序規則:禁止對final域的寫重排序到構造函數以外,這個規則的實現主要包含了兩個方面:

  • JMM禁止編譯器把final域的寫重排序到構造函數以外

  • 編譯器會在final域寫以後,構造函數return以前,插入一個StoreStore屏障。 這個屏障能夠禁止處理器把final域的寫重排序到構造函數以外。 (參見 StoreStore Barriers的說明:在Store1;Store2之間插入StoreStore,確保Store1對 其餘處理器可見(刷新內存)先於Store2及全部後續存儲指令的存儲)

writer方法中,實際上作了兩件事:

  • 構造了一個FinalDemo對象

  • 把這個對象賦值給成員變量finalDemo

可能的執行時序圖以下:

06_00.png


a,b之間沒有數據依賴性,普通域(普通變量)a可能會被重排序到構造函數以外, 線程B就有可能讀到的是普通變量a初始化以前的值(零值),這樣就可能出現錯誤。

final域變量b,根據重排序規則,會禁止final修飾的變量b重排序到構造函數以外,從而b可以正確賦值, 線程B就可以讀到final變量初始化後的值。

所以,寫final域的重排序規則能夠確保:在對象引用爲任意線程可見以前,對象的final域已經被正確初始化過了。 普通域不具備這個保障,好比在上例,線程B有可能就是一個未正確初始化的對象finalDemo。

讀final域重排序規則

讀final域重排序規則:在一個線程中,初次讀對象引用和初次讀該對象包含的final域,JMM會禁止這兩個操做的重排序。 (注意,這個規則僅僅是針對處理器), 處理器會在讀final域操做的前面插入一個LoadLoad屏障。 實際上,讀對象的引用和讀該對象的final域存在間接依賴性,通常處理器不會重排序這兩個操做。 可是有一些處理器會重排序,所以,這條禁止重排序規則就是針對這些處理器而設定的。

read方法主要包含了三個操做:

  • 初次讀引用變量finalDemo

  • 初次讀引用變量finalDemo的普通域a

  • 初次讀引用變量finalDemo的final域b

假設線程A寫過程沒有重排序,那麼線程A和線程B有一種的可能執行時序以下:

06_01.png


讀對象的普通域被重排序到了讀對象引用的前面就會出現線程B還未讀到對象引用就在讀取該對象的普通域變量,這顯然是錯誤的操做。

final域的讀操做就「限定」了在讀final域變量前已經讀到了該對象的引用,從而就能夠避免這種狀況。

所以,讀final域的重排序規則能夠確保:在讀一個對象的final域以前,必定會先讀這個包含這個final域的對象的引用。

final爲引用類型

public class FinalReferenceDemo {    final int[] arrays; //arrays是引用類型
    private FinalReferenceDemo finalReferenceDemo;    public FinalReferenceDemo() {
        arrays = new int[1];  //1 
        arrays[0] = 1;        //2
    }    public void writerOne() {
        finalReferenceDemo = new FinalReferenceDemo(); //3
    }    public void writerTwo() {
        arrays[0] = 2;  //4
    }    public void reader() {        if (finalReferenceDemo != null) {  //5
            int temp = finalReferenceDemo.arrays[0];  //6
        }
    }
}複製代碼

對final修飾的對象的成員域進行寫操做

針對引用數據類型,final域寫針對編譯器和處理器重排序增長了這樣的約束: 在構造函數內對一個final修飾的對象的成員域的寫入,與隨後在構造函數以外把這個被構造的對象的引用賦給一個引用變量,這兩個操做是不能被重排序的。 注意這裏的是「增長」也就說前面對final基本數據類型的重排序規則在這裏仍是使用。

線程線程A執行wirterOne方法,執行完後線程B執行writerTwo方法,線程C執行reader方法。 下圖就以這種執行時序出現的一種狀況來討論:

06_02.png


對final域的寫禁止重排序到構造方法外,所以1和3不能被重排序。 因爲一個final域的引用對象的成員域寫入不能與在構造函數以外將這個被構造出來的對象賦給引用變量重排序, 所以2和3不能重排序。

對final修飾的對象的成員域進行讀操做

JMM能夠確保線程C至少能看到寫線程A對final引用的對象的成員域的寫入,即能看到arrays[0] = 1,而 寫線程B對數組元素的寫入可能看到可能看不到。 JMM不保證線程B的寫入對線程C可見,線程B和線程C之間存在數據競爭,此時的結果是不可預知的。 若是想要可見,可以使用鎖或者volatile。

final重排序的總結


final寫 final域讀
基本數據類型 禁止final域寫與構造方法重排序,即禁止final域寫重排序到構造方法以外,從而保證該對象對全部線程可見時,該對象的final域所有已經初始化過 禁止初次讀對象的引用與讀該對象包含的final域的重排序,保證了在讀一個對象的final域以前,必定會先讀這個包含這個final域的對象的引用
引用數據類型 額外增長約束:構造函數內對一個final修飾的對象的成員域的寫入,與隨後在構造函數以外把這個被構造的對象的引用賦給一個引用變量,這兩個操做是不能被重排序的

對象溢出

對象溢出:一種錯誤的發佈,當一個對象尚未構造完成時,就使它被其餘線程所見。

/*** 對象溢出示例*/public class ThisEscape {
  public ThisEscape(EventSource source) {
    source.registerListener(new EventListener() {
      public void onEvent(Event e) {
        doSomething(e);
      }
    });
  }
 
  void doSomething(Event e) {
  }
}複製代碼

這將致使this逸出,所謂逸出,就是在不應發佈的時候發佈了一個引用。

在這個例子裏面,當咱們實例化ThisEscape對象時,會調用source的registerListener方法, 這時便啓動了一個線程,並且這個線程持有了ThisEscape對象(調用了對象的doSomething方法), 但此時ThisEscape對象卻沒有實例化完成(尚未返回一個引用),因此咱們說, 此時形成了一個this引用逸出,即尚未完成的實例化ThisEscape對象的動做,卻已經暴露了對象的引用。

正確構造過程:

public class SafeListener {
  private final EventListener listener;
 
  private SafeListener() {
    listener = new EventListener() {
      public void onEvent(Event e) {
        doSomething(e);
      }
    };
  }
 
  public static SafeListener newInstance(EventSource source) {
    SafeListener safe = new SafeListener();
    source.registerListener(safe.listener);
    return safe;
  }
 
  void doSomething(Event e) {
  }複製代碼

當構造好了SafeListener對象(經過構造器構造)以後, 咱們才啓動了監聽線程,也就確保了SafeListener對象是構造完成以後再使用的SafeListener對象。

結論:

  • 只有當構造函數返回時,this引用才應該從線程中逸出。

  • 構造函數能夠將this引用保存到某個地方,只要其餘線程不會在構造函數完成以前使用它。

final的實現原理

寫final域會要求編譯器在final域寫以後,構造函數返回前插入一個StoreStore屏障。

讀final域的重排序規則會要求編譯器在讀final域的操做前插入一個LoadLoad屏障。

若是以X86處理爲例,X86不會對寫-寫重排序,因此StoreStore屏障能夠省略。 因爲不會對有間接依賴性的操做重排序,因此在X86處理器中,讀final域須要的LoadLoad屏障也會被省略掉。 也就是說,以X86爲例的話,對final域的讀/寫的內存屏障都會被省略!具體是否插入仍是得看是什麼處理器。

  • 注意:

上面對final域寫重排序規則能夠確保咱們在使用一個對象引用的時候該對象的final域已經在構造函數被初始化過了。 可是這裏實際上是有一個前提條件: 在構造函數,不能讓這個被構造的對象被其餘線程可見,也就是說該對象引用不能在構造函數中「溢出」。

public class FinalReferenceEscapeDemo {    private final int a;    private FinalReferenceEscapeDemo referenceDemo;    public FinalReferenceEscapeDemo() {
        a = 1;  //1
        referenceDemo = this; //2
    }    public void writer() {        new FinalReferenceEscapeDemo();
    }    public void reader() {        if (referenceDemo != null) {  //3
            int temp = referenceDemo.a; //4
        }
    }
}複製代碼

06_03.png


假設一個線程A執行writer方法另外一個線程執行reader方法。由於構造函數中操做1和2之間沒有數據依賴性,1和2能夠重排序,先執行了2,這個時候引用對象referenceDemo是個沒有徹底初始化的對象,而當線程B去讀取該對象時就會出錯。儘管依然知足了final域寫重排序規則:在引用對象對全部線程可見時,其final域已經徹底初始化成功。 可是,引用對象「this」逸出,該代碼依然存在線程安全的問題。

相關文章
相關標籤/搜索