
面試題:用過final關鍵字嗎?它有什么作用
面試考察點
考察目的: 了解面試者對Java基礎知識的理解
考察人群: 作業1-5年,作業年限越高,對于基礎知識理解的深度就越高,
背景知識
final關鍵字大家都不陌生,但是要達到深度理解,還是欠缺了一些,我們從三個方面去理解final關鍵字,
final關鍵字的基本用法- 深度理解
final關鍵字 final關鍵字的記憶體屏障語意
final的基本用法
final關鍵字,在Java中可以修飾類、方法、變數,
-
被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進行繼承 -
被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 -
被final修飾的成員變數是用得最多的地方,
- 對于一個final變數,如果是基本資料型別的變數,則其數值一旦在初始化之后便不能更改;final修飾的變數能間接實作常量的功能,而常量是全域的、不可變的,因此我們同時使用static和final來修飾變數,就能達到定義常量的效果,
- 如果是參考型別的變數,則在對其初始化之后便不能再讓其指向另一個物件,
被final修飾的變數的初始化
-
在定義時初始化屬性的值
public class TCCClass { private final String name; public static void main(String[] args) { } }上述代碼在運行時會提示如下錯誤
java: 變數 name 未在默認構造器中初始化修改成下面的方式即可,
public class TCCClass { private final String name="name"; } -
在構造方法中賦值
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(匿名內部類)

通過反編譯來看一下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以引數的形式傳進來,對匿名內部類中的拷貝(變數a和b的拷貝)進行賦值初始化,
也就是說,在run方法中訪問的變數a和b,是區域變數a和b的一個副本,為什么這么設計?
在
test方法中,有可能test方法執行結束且a和b的宣告周期也結束了,但是Thread這個匿名內部類可能還未執行完,那么在Thread中的run方法中繼續使用區域變數a和b就會有問題,但是又要實作這樣的效果,怎么辦呢?所以Java采用了復制的手段來解決這個問題,
但是這樣一來,還是存在一個問題,就是test方法中的成員變數與匿名內部類Thread中的成員變數的副本出現資料不一致怎么辦?
這樣就達不到原本的意圖和要求,為了解決這個問題,java編譯器就限定必須將變數a和b限制為final變數,不允許對變數a和b進行更改(對于參考型別的變數,是不允許指向新的物件),這樣資料不一致性的問題就得以解決了,
另外,如果我們這么寫也是允許的,jvm會隱式給a和b增加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 變數的寫與讀,
- 寫的重排序規則可以保證,在物件參考對任意執行緒可見之前,物件的 final 變數已經正確初始化了,而普通變數則不具有這個保障;
- 讀的重排序規則可以保證,在讀一個物件的 final 變數之前,一定會先讀這個物件的參考,如果讀取到的參考不為空,根據上面的寫規則,說明物件的 final 變數一定以及初始化完畢,從而可以讀到正確的變數值,
如果 final 變數的型別是參考型,那么建構式內,對一個 final 參考的物件的成員域的寫入,與隨后在建構式外把這個被構造物件的參考賦值給一個參考變數,這兩個操作之間不能重排序,實際上這也是為了保證 final 變數在對其他執行緒可見之前,能夠正確的初始化完成,
關于指令重排序相關的內容,就不在本篇文章中做展開,在后續的面試題中,會做詳細的分析,
final 關鍵字的好處
下面為使用 final 關鍵字的一些好處:
- final關鍵字提高了性能,JVM和Java應用都會快取final變數(實際就是常量池)
- final變數可以安全的在多執行緒環境下進行共享,而不需要額外的同步開銷
問題解答
面試題:用過final關鍵字嗎?它有什么作用
回答: final關鍵字表示不可變,它可以修飾在類、方法、成員變數中,
- 如果修飾在類上,則表示該類不允許被繼承
- 修飾在方法上,表示該方法無法被重寫
- 修飾在變數上,表示該變數無法被修改,而且JVM會隱性定義為一個常量,
另外,final修飾的關鍵字,還可以避免因為指令重排序帶來的可見性問題,原因是,final遵循兩個重排序規則
- 建構式內,對一個 final 變數的寫入,與隨后把這個被構造物件的參考賦值給一個變數,這兩個操作之間不可重排序,
- 首次讀一個包含 final 變數的物件,與隨后首次讀這個 final 變數,這兩個操作之間不可以重排序,
問題總結
恰恰是平時經常使用的一些工具或者技術,所涉及到的知識點越多,
就這個問題來說,在面試時的考察點太多了,比如:
- 如何破壞final規則
- 帶static和final修飾的屬性,可以被修改嗎?
- final是否可以解決可見性問題,以及它是如何解決的?
因此,要想在面試時從容應對,一定要具備體系化的技術理解,避免面試時各種”不清楚“、”不了解“之類的尷尬!
關注[跟著Mic學架構]公眾號,獲取更多精品原創

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/345477.html
標籤:Java
下一篇:分布式事務(二)之三階段提交
