深入剖析Java關(guān)鍵字之final

一、摘要

?我們大家都知道,Java中平時(shí)用的比較多的String類型是不可以被繼承的,因?yàn)镾tring類有final修飾,來看下String類的定義:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence

?很明顯有一個(gè)final的關(guān)鍵字,那么在Java中,final有哪些作用?到底怎么實(shí)現(xiàn)的呢?內(nèi)存語義是啥呢?接下來的文章將重點(diǎn)分析final的作用以及內(nèi)存語義等等。


二、final關(guān)鍵字的作用

?根據(jù)上下文環(huán)境,Java的關(guān)鍵字final的含義存在著細(xì)微的區(qū)別,但通常它指的是“這是無法改變的?!辈幌胱龈淖兛赡艹鲇趦煞N理由:設(shè)計(jì)或效率。由于這兩個(gè)原因相差很遠(yuǎn),所以關(guān)鍵字final有可能被誤用。
?基于Java語言規(guī)范,我們知道final可以修飾變量、方法以及類,接下來我們分別介紹final修飾它們的作用。

2.1 final修飾變量

?先來看下Java語言規(guī)范中關(guān)于final修飾變量的描述:

?變量可以被聲明為final,而final變量只能被賦值一次。如果對(duì)final變量賦值,那么除非在賦值之前該變量是明確未賦值的,否則就是一種編譯時(shí)錯(cuò)誤。
?一旦final變量被賦值,那么它就始終有同一個(gè)值。如果一個(gè)final變量持有的是對(duì)象的引用,那么該對(duì)象的狀態(tài)可以被對(duì)象上的操作所修改,但是該變量會(huì)始終指向這個(gè)對(duì)象。這條規(guī)則也同樣適用于數(shù)組,因?yàn)閿?shù)組也是對(duì)象。如果一個(gè)final變量持有的是指向數(shù)組的引用,那么該數(shù)組的元素可以被數(shù)組上的操作所修改,但是該變量會(huì)始終指向這個(gè)數(shù)組。
?空final是指其聲明缺少初始化器的final變量。
?常量變量是指用常量表達(dá)式初始化的簡單類型或String類型的final變量。
?有三種變量被隱式地聲明為final:接口的域、帶資源的try語句中的資源,以及多重catch子句中的異常參數(shù)。單catch子句的異常參數(shù)永遠(yuǎn)都不會(huì)被隱式地聲明為final,但是它可以被認(rèn)為效果等同于final。

?從規(guī)范中,可以看出來final變量只能被賦值一次;而變量我們分為基本數(shù)據(jù)類型和引用類型的變量,對(duì)于基本數(shù)據(jù)類型的變量:final使數(shù)值恒定不變;對(duì)于引用類型的final變量:final使引用恒定不變。一旦引用被初始化指向一個(gè)對(duì)象,就無法再把它改為指向另一個(gè)對(duì)象。然后,對(duì)象自身卻是可以被修改的,Java并未提供使任何對(duì)象恒定不變的途徑(但可以自己編寫類以取得對(duì)象恒定不變的效果)。這一限制同樣適用于數(shù)組,它也是對(duì)象。
?一個(gè)既是static又是final的域只占據(jù)一段不能改變的存儲(chǔ)空間。
?下面的例子示范了final域的情況。

import java.util.Random;

class Value {
  int i; // Package access
  public Value(int i) { this.i = i; }
}

public class FinalData {
  private static Random rand = new Random(47);
  private String id;
  public FinalData(String id) {
    this.id = id;
  }
  // Can be compile-time constants:
  private final int valueOne = 9;
  private static final int VALUE_TWO = 99;
  // Typical public constant:
  public static final int VALUE_THREE = 39;
  // Cannot be compile-time constants:
  private final int i4 = rand.nextInt(20);
  static final int INT_5 = rand.nextInt(20);
  private Value v1 = new Value(11);
  private final Value v2 = new Value(22);
  private static final Value VAL_3 = new Value(33);
  // Arrays:
  private final int[] a = { 1, 2, 3, 4, 5, 6 };
  public String toString() {
    return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5;
  }
  public static void main(String[] args) {
    FinalData fd1 = new FinalData("fd1");
    //! fd1.valueOne++; // Error: can't change value
    fd1.v2.i++; // Object isn't constant!
    fd1.v1 = new Value(9); // OK -- not final
    for(int i = 0; i < fd1.a.length; i++)
      fd1.a[i]++; // Object isn't constant!
    //! fd1.v2 = new Value(0); // Error: Can't
    //! fd1.VAL_3 = new Value(1); // change reference
    //! fd1.a = new int[3];
    System.out.println(fd1);
    System.out.println("Creating new FinalData");
    FinalData fd2 = new FinalData("fd2");
    System.out.println(fd1);
    System.out.println(fd2);
  }
} /* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*///:~

?由于valueOne和VAL_TWO都是帶編譯時(shí)數(shù)值的final基本類型,所以它們二者均可以用作編譯器常量,并且沒有特別大的區(qū)別。VAL_THREE是一種更加典型的對(duì)常量進(jìn)行定義的方式:定義為public,則可以被用于包之外;定義為static,則強(qiáng)調(diào)只有一份;定義為final,則說明它是一個(gè)常量。請(qǐng)注意,帶有恒定初始值(即編譯器常量)的final static基本類型全用大寫字母命名,并且字與字之間用下劃線隔開。
?我們不能因?yàn)槟硵?shù)據(jù)是final的就認(rèn)為在編譯時(shí)可以知道它的值。在運(yùn)行時(shí)使用隨機(jī)生成的數(shù)值來初始化i4和INT_5就說明了這一點(diǎn)。示例部分也展示了將final數(shù)值定義為靜態(tài)和費(fèi)靜態(tài)的區(qū)別。此區(qū)別只有當(dāng)數(shù)值在運(yùn)行時(shí)被初始化時(shí)才會(huì)顯現(xiàn),這是因?yàn)榫幾g器對(duì)編譯時(shí)數(shù)值一視同仁。當(dāng)運(yùn)行程序時(shí)就會(huì)看到這個(gè)區(qū)別。注意,在fd1和fd2中,i4的值是唯一的,但I(xiàn)NT_5的值是不可以通過創(chuàng)建第二個(gè)FinalData對(duì)象而加以改變的。這是因?yàn)樗莝tatic的,在裝載時(shí)已被初始化,而不是每次創(chuàng)建新對(duì)象時(shí)都初始化。
?v1到VAL_3這些變量說明了final引用的意義。正如在main()中所看到的,不能因?yàn)関2是final的,就認(rèn)為無法改變它的值。由于它是一個(gè)引用,final意味著無法將v2再次指向另一個(gè)新的對(duì)象。

2.2 空白final

?Java允許生成“空白final”,所謂空白final是指被聲明為final但又未給定初始值的域。無論什么情況,編譯器都確??瞻譮inal在使用前必須被初始化。但是,空白final在關(guān)鍵字final的使用上提供了更大的靈活性,為此,一個(gè)類中的final域就可以做到根據(jù)對(duì)象而有所不同,卻又保持其恒定不變的特性。例如:

class Poppet {
  private int i;
  Poppet(int ii) {
    i = ii;
  }
  public int getI() {
    return i;
  }
}

public class BlankFinal {
  private final int i = 0; // Initialized final
  private final int j; // Blank final
  private final Poppet p; // Blank final reference
  // Blank finals MUST be initialized in the constructor:
  public BlankFinal() {
    j = 1; // Initialize blank final
    p = new Poppet(1); // Initialize blank final reference
  }
  public BlankFinal(int x) {
    j = x; // Initialize blank final
    p = new Poppet(x); // Initialize blank final reference
  }
  public static void main(String[] args) {
    BlankFinal bf1 = new BlankFinal();
    BlankFinal bf2 = new BlankFinal(47);
    System.out.println(bf1.p.getI());
    System.out.println(bf2.p.getI());
  }
} /* Output:
1
47
*///:~

?必須在域的定義處或者每個(gè)構(gòu)造器中用表達(dá)式對(duì)final進(jìn)行賦值,這正是final域在使用前總是被初始化的原因所在。

2.3 final參數(shù)

?Java允許在參數(shù)列表中以聲明的方式將參數(shù)知名為final。這意味著你無法再方法中更改參數(shù)引用所指向的對(duì)象:

class Gizmo {
  public void spin() {

  }
}

public class FinalArguments {
  void with(final Gizmo g) {
    //! g = new Gizmo(); // Illegal -- g is final
  }
  void without(Gizmo g) {
    g = new Gizmo(); // OK -- g not final
    g.spin();
  }
  // void f(final int i) { i++; } // Can't change
  // You can only read from a final primitive:
  int g(final int i) {
    return i + 1;
  }
  public static void main(String[] args) {
    FinalArguments bf = new FinalArguments();
    bf.without(null);
    bf.with(null);
  }
} ///:~

?方法f()和g()展示了當(dāng)基本類型的參數(shù)被指明為final時(shí)所出現(xiàn)的結(jié)果:你可以讀參數(shù),但無法修改參數(shù)。這一特性主要用來想匿名內(nèi)部類傳遞數(shù)據(jù)。

2.4 final方法

?使用final方法的原因有兩個(gè)。第一個(gè)原因是把方法鎖定,以防任何繼承類修改它的含義。這是出于設(shè)計(jì)的考慮:想要確保在繼承中使方法行為保持不變,并且不會(huì)被覆蓋。
?過去建議使用final方法的第二個(gè)原因是效率,在Java的早期實(shí)現(xiàn)中,如果將一個(gè)方法指明為final,就是同意編譯器將針對(duì)該方法的所有調(diào)用都轉(zhuǎn)為內(nèi)聯(lián)調(diào)用。當(dāng)編譯器發(fā)現(xiàn)一個(gè)final方法調(diào)用命令時(shí),它會(huì)根據(jù)自己的謹(jǐn)慎判斷,跳過插入程序代碼這種正常方式而執(zhí)行方法調(diào)用機(jī)制,并且以方法體重的時(shí)機(jī)代碼的副本來替代方法調(diào)用。這將消除方法調(diào)用的開銷。當(dāng)然,如果一個(gè)方法很大,你的程序代碼就會(huì)膨脹,因而可能看不到內(nèi)聯(lián)帶來的任何性能提高,因?yàn)?,所帶來的性能提高?huì)因?yàn)榛ㄙM(fèi)于方法內(nèi)的時(shí)間量而被縮減。
?在最新的Java版本中,虛擬機(jī)(特別是hotspot技術(shù))可以探測(cè)到這些情況,并優(yōu)化去掉這些效率反而降低的額外的內(nèi)聯(lián)調(diào)用,因此不再需要使用final方法來進(jìn)行優(yōu)化了。在使用新的Java版本時(shí),應(yīng)該讓編譯器和JVM去處理效率問題,只有在想要明確禁止覆蓋時(shí),才將方法設(shè)置為final的。

fina和private關(guān)鍵字

?類中所有的private方法都隱式地指定為是final的。由于無法取用private方法,所以也就無法覆蓋它??梢詫?duì)private方法添加final修飾詞,但這并不給該方法增加任何額外的意義。

2.5 final類

?當(dāng)將某個(gè)類的整體定義為final時(shí)(通過將關(guān)鍵字final置于它的定義之前),就表明了你不打算繼承該類,而且也不允許別人這樣做。換句話說,出于某種考慮,你對(duì)該類的設(shè)計(jì)用不需要做任何變動(dòng),或者處于安全的考慮,你不希望它有子類。

class SmallBrain {}

final class Dinosaur {
  int i = 7;
  int j = 1;
  SmallBrain x = new SmallBrain();
  void f() {}
}

//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
  public static void main(String[] args) {
    Dinosaur n = new Dinosaur();
    n.f();
    n.i = 40;
    n.j++;
  }
} ///:~

?請(qǐng)注意,final類的域可以根據(jù)個(gè)人的意愿選擇為是或不是final。不論類是否被定義為final,相同的規(guī)則都適用于定義為final的域。然而,由于final類禁止繼承,所以final類中所有的方法都隱式指定為是final的,因?yàn)闊o法覆蓋它們。在final類中可以給方法添加final修飾詞,但這不會(huì)增添任何意義。


三、final的原理


四、參考引用

Bruce Eckel 《Java編程思想(第四版)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容