面試題系列:工作5年,第一次這么清醒的理解final關鍵字?


圖怪獸_4d9e52c6f1c95d5bdb266b4ae0504363_61669

面試題:用過final關鍵字嗎?它有什么作用

面試考察點

考察目的: 了解面試者對Java基礎知識的理解

考察人群: 工作1-5年,工作年限越高,對於基礎知識理解的深度就越高。

背景知識

final關鍵字大家都不陌生,但是要達到深度理解,還是欠缺了一些。我們從三個方面去理解final關鍵字。

  1. final關鍵字的基本用法
  2. 深度理解final關鍵字
  3. final關鍵字的內存屏障語義

final的基本用法

final關鍵字,在Java中可以修飾類、方法、變量。

  1. 被final修飾的類,表示這個類不可被繼承,final類中的成員變量可以根據需要設為final,並且final修飾的類中的所有成員方法都被隱式指定為final方法.

    在使用final修飾類的時候,要注意謹慎選擇,除非這個類真的在以后不會用來繼承或者出於安全的考慮,盡量不要將類設計為final類。

    public final class TClass {
    
        public final String test(){
            return "true";
        }
    }
    public class TCCClass extends TClass{
    
    
        public static void main(String[] args) {
    
        }
    }
    

    上述程序運行得到如下錯誤:

    java: 無法從最終org.example.cl03.TClass進行繼承
    
  2. 被final修飾的方法,表示該方法無法被重寫.其中private方法會被隱式的指定為final方法。

    class SuperClass{
       protected final String getName() {
           return “supper class”;
       }
    
       @Override
        public String toString() {
            return getName();
        }
    }
    
    classSubClass extends SuperClass{
      protected String getName() {
          return “sub class”;
      }
    } 
    

    上述代碼運行會得到如下錯誤:

    java: org.example.cl03.TCCClass中的test()無法覆蓋org.example.cl03.TClass中的test()
      被覆蓋的方法為final
    
  3. 被final修飾的成員變量是用得最多的地方。

    1. 對於一個final變量,如果是基本數據類型的變量,則其數值一旦在初始化之后便不能更改;final修飾的變量能間接實現常量的功能,而常量是全局的、不可變的,因此我們同時使用static和final來修飾變量,就能達到定義常量的效果。
    2. 如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。

被final修飾的變量的初始化

  1. 在定義時初始化屬性的值

    public class TCCClass {
        private final String name;
        public static void main(String[] args) {
    
        }
    }
    

    上述代碼在運行時會提示如下錯誤

    java: 變量 name 未在默認構造器中初始化
    

    修改成下面的方式即可。

    public class TCCClass {
        private final String name="name";
    }
    
  2. 在構造方法中賦值

    public class TCCClass {
        private final String name;
        public TCCClass(String name){
            this.name=name;
        }
    }
    

能夠在構造方法中賦值的原因是:對於一個普通成員屬性賦值時,必須要先通過構造方法實例化該對象。因此作為該屬性唯一的訪問入口,JVM允許在構造方法中給final修飾的屬性賦值。這個過程並沒有違反final的原則。當然如果被修飾final關鍵字的屬性已經初始化了值,是無法再使用構造方法重新賦值的。

反射破壞final規則

基於上述final關鍵字的基本使用描述,可以知道final修飾的屬性是不可變的。

但是,通過反射機制,可以破壞final的規則,代碼如下

public class TCCClass {
    private final String name="name";

    public static void main(String[] args) throws Exception {
        TCCClass tcc=new TCCClass();
        System.out.println(tcc.name);
        Field name=tcc.getClass().getDeclaredField("name");
        name.setAccessible(true);
        name.set(tcc,"mic");
        System.out.println(name.get(tcc));
    }
}

打印結果如下:

name
mic

知識點擴展

上述代碼理論上來說應該是下面這種寫法,因為通過反射修改tcc實例對象中的name屬性后,應該通過實例對象直接打印出name的結果。

public static void main(String[] args) throws Exception {
  TCCClass tcc=new TCCClass();
  System.out.println(tcc.name);
  Field name=tcc.getClass().getDeclaredField("name");
  name.setAccessible(true);
  name.set(tcc,"mic");
  System.out.println(tcc.name); //here
}

但是實際輸出結果后,發現tcc.name打印的結果沒有變化?

原因是:JVM在編譯時期做的深度優化機制, 就把final類型的String進行了優化, 在編譯時期就會把String處理成常量,導致打印結果不會發生變化。

為了避免這種深度優化帶來的影響,我們還可以把上述代碼修改成下面這種形式

public class TCCClass {
    private final String name=(null == null ? "name" : "");

    public static void main(String[] args) throws Exception {
        TCCClass tcc=new TCCClass();
        System.out.println(tcc.name);
        Field name=tcc.getClass().getDeclaredField("name");
        name.setAccessible(true);
        name.set(tcc,"mic");
        System.out.println(tcc.name);
    }
}

打印結果如下:

name
mic

反射無法修改被final和static同時修飾的變量

把上面的代碼修改如下。

public class TCCClass {
    private static final String name=(null == null ? "name" : "");

    public static void main(String[] args) throws Exception {
        TCCClass tcc=new TCCClass();
        System.out.println(tcc.name);
        Field name=tcc.getClass().getDeclaredField("name");
        name.setAccessible(true);
        name.set(tcc,"mic");
        System.out.println(tcc.name);
    }
}

執行結果,執行之后會報出如下異常, 因為反射無法修改同時被static final修飾的變量:

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final java.lang.String field org.example.cl03.TCCClass.name to java.lang.String
	at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)
	at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)
	at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
	at java.lang.reflect.Field.set(Field.java:764)
	at org.example.cl03.TCCClass.main(TCCClass.java:13)

那么被final和static同時修飾的屬性,能否被修改呢?答案是可以的!

修改代碼如下:

public class TCCClass {
    private static final String name=(null == null ? "name" : "");

    public static void main(String[] args) throws Exception {
        TCCClass tcc=new TCCClass();
        System.out.println(tcc.name);
        Field name=tcc.getClass().getDeclaredField("name");
        name.setAccessible(true);

        Field modifiers = name.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);

        name.set(tcc,"mic");

        modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);

        System.out.println(tcc.name);
    }
}

具體思路是,把被修飾了final關鍵字的name屬性,通過反射的方式去掉final關鍵字,代碼實現

Field modifiers = name.getClass().getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);

接着通過反射修改name屬性,修改成功后,再使用下面代碼把final關鍵字加回來

modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);

為什么局部內部類和匿名內部類只能訪問final變量

在了解這個問題之前,我們先來看下面這段代碼

    public static void main(String[] args)  {

    }

    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
                System.out.println(b);
            };
        }.start();
    }
}

這段代碼被編譯后,會生成兩個文件: FinalExample.class和FinalExample$1.class(匿名內部類)

image-20211102124604099

通過反編譯來看一下FinalExample$1.class這個類

class FinalExample$1 extends Thread {
    FinalExample$1(FinalExample this$0, int var2, int var3) {
        this.this$0 = this$0;
        this.val$a = var2;
        this.val$b = var3;
    }

    public void run() {
        System.out.println(this.val$a);
        System.out.println(this.val$b);
    }
}

我們看到匿名內部類FinalExample$1的構造器含有三個參數,一個是指向外部類對象的引用,另外兩個是int型變量,很顯然,這里是將變量test方法中的形參b,以及常量a以參數的形式傳進來,對匿名內部類中的拷貝(變量ab的拷貝)進行賦值初始化。

也就是說,在run方法中訪問的變量ab,是局部變量ab的一個副本,為什么這么設計?

test方法中,有可能test方法執行結束且ab的聲明周期也結束了,但是Thread這個匿名內部類可能還未執行完,那么在Thread中的run方法中繼續使用局部變量ab就會有問題。但是又要實現這樣的效果,怎么辦呢?所以Java采用了復制的手段來解決這個問題。

但是這樣一來,還是存在一個問題,就是test方法中的成員變量與匿名內部類Thread中的成員變量的副本出現數據不一致怎么辦?

這樣就達不到原本的意圖和要求。為了解決這個問題,java編譯器就限定必須將變量ab限制為final變量,不允許對變量ab進行更改(對於引用類型的變量,是不允許指向新的對象),這樣數據不一致性的問題就得以解決了。

另外,如果我們這么寫也是允許的,jvm會隱式給ab增加final關鍵字。

public void test(int b) {
  int a = 10;
  new Thread(){
    public void run() {
      System.out.println(a);
      System.out.println(b);
    };
  }.start();
}

final防止指令重排

final關鍵字,還能防止指令重排序帶來的可見性問題;

對於final變量,編譯器和處理器都要遵守兩個重排序規則:

  • 構造函數內,對一個 final 變量的寫入,與隨后把這個被構造對象的引用賦值給一個變量,這兩個操作之間不可重排序。
  • 首次讀一個包含 final 變量的對象,與隨后首次讀這個 final 變量,這兩個操作之間不可以重排序。

實際上這兩個規則也正是針對 final 變量的寫與讀。

  1. 寫的重排序規則可以保證,在對象引用對任意線程可見之前,對象的 final 變量已經正確初始化了,而普通變量則不具有這個保障;
  2. 讀的重排序規則可以保證,在讀一個對象的 final 變量之前,一定會先讀這個對象的引用。如果讀取到的引用不為空,根據上面的寫規則,說明對象的 final 變量一定以及初始化完畢,從而可以讀到正確的變量值。

如果 final 變量的類型是引用型,那么構造函數內,對一個 final 引用的對象的成員域的寫入,與隨后在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操作之間不能重排序。實際上這也是為了保證 final 變量在對其他線程可見之前,能夠正確的初始化完成。

關於指令重排序相關的內容,就不在本篇文章中做展開,在后續的面試題中,會做詳細的分析。

final 關鍵字的好處

下面為使用 final 關鍵字的一些好處:

  • final關鍵字提高了性能,JVM和Java應用都會緩存final變量(實際就是常量池)
  • final變量可以安全的在多線程環境下進行共享,而不需要額外的同步開銷

問題解答

面試題:用過final關鍵字嗎?它有什么作用

回答: final關鍵字表示不可變,它可以修飾在類、方法、成員變量中。

  1. 如果修飾在類上,則表示該類不允許被繼承
  2. 修飾在方法上,表示該方法無法被重寫
  3. 修飾在變量上,表示該變量無法被修改,而且JVM會隱性定義為一個常量。

另外,final修飾的關鍵字,還可以避免因為指令重排序帶來的可見性問題,原因是,final遵循兩個重排序規則

  1. 構造函數內,對一個 final 變量的寫入,與隨后把這個被構造對象的引用賦值給一個變量,這兩個操作之間不可重排序。
  2. 首次讀一個包含 final 變量的對象,與隨后首次讀這個 final 變量,這兩個操作之間不可以重排序。

問題總結

恰恰是平時經常使用的一些工具或者技術,所涉及到的知識點越多。

就這個問題來說,在面試時的考察點太多了,比如:

  1. 如何破壞final規則
  2. 帶static和final修飾的屬性,可以被修改嗎?
  3. final是否可以解決可見性問題,以及它是如何解決的?

因此,要想在面試時從容應對,一定要具備體系化的技術理解,避免面試時各種”不清楚“、”不了解“之類的尷尬!

關注[跟着Mic學架構]公眾號,獲取更多精品原創


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM