天天看點

java關鍵字final淺談

上一篇我們提到關鍵finally,這一篇我們來看看final。這兩者差別很大的。

final可以修飾:資料、方法和類

final資料

在我們程式設計中,往往會涉及到一些常量, 常量是程式運作時恒定不變的量,許多程式設計語言都有某種方法,向編譯器告知一塊資料是恒定不變的,例如C++中的const和Java中的final。而常量主要應用下述兩個方面:

(1)編譯期常數,它永遠不會改變 (編譯時常量)

public class FinalTest {
    //編譯時常量
    final int i1 =;
    static final int I2 = ; //變量需大寫
    public void method(){
    }
    public static void main(String[] args) {
        FinalTest fd1 = new FinalTest();
        //fd1.i1++; // 編譯報錯不能改變值
        //FinalTest.I2++;// 編譯報錯不能改變值
    }
}
           

對于編譯器的常量,編譯器可将常量值“封裝”到需要的技術過程裡。也就是說計算時間提前執行,進而節省運作時的一些開銷。比如我們常用的常量類。在java中,這些形式的常數必須屬于基本類型,而且要用final關鍵字進行表達。并且在定義時給出一個值。

注意:對于含有固定初始化值(即編譯期常數)的 fianl static 基本資料類型,它們的名字根據規則要全部采用大寫

(2)在運作期初始化的一個值,我們不希望它發生變化(運作時常量)

public class FinalTest {
    //運作期常量
    int i3 = (int)(Math.random()*);
    final int i4 = (int)(Math.random()*);
    static final int i5 = (int)(Math.random()*); 

    public void print(String id){
        System.out.println(
                id + ": " + "i4 = " + i4 +
                ", i5 = " + i5);
    }
    public static void main(String[] args) {
        FinalTest fd1 = new FinalTest();
        fd1.print("fd1");
        FinalTest fd2 = new FinalTest();
        fd1.print("fd1");
        fd2.print("fd2");
        fd2.print("fd1");
    }
}
運作結果:
fd1: i4 = , i5 = 
fd1: i4 = , i5 = 
fd2: i4 = , i5 = 
fd1: i4 = , i5 = 
           

從上面的列子中可以看出,并非說我用final修飾了某樣東西,那麼它的值就一定能在編譯期确定。i4和i5證明了這一點,它們是在運作期使用的随機數。例子的這一部分也向大家揭示出将final 值設為 static 和非 static 之間的差異。隻有當值在運作期間初始化的前提下,這種差異才會揭示出來。

從結果我們可以知道,對于i4當同一個對象調用時,不管調用多少次,它的值會一直保持為13。而對于i5的值不會由于建立了另一個 FinalData 對象而發生改變。那是因為它的屬性是 static,而且在載入時初始化,而非每建立一個對象時初始化。

注意:i5 在編譯期間是未知的,是以它沒有大寫。

被final修飾的基本資料類型和對象的差別

對于基本資料類型,一旦被final修飾,那麼它的值将不會再發送改變。對于final修飾的變量必須給定一個值。

而對于一個對象就沒那麼簡單了,我們知道一旦我們new了一個對象,編譯器會在堆空間中給這個對象配置設定一塊空間,我們通過一個指引指向這個對象。一旦對象指引被final修飾,那麼它的指引就會被固定不再改變(也就是說這個指引永遠不能指向另一個對象),但需要注意的是對象本身是可以改變的。這點也是容易被許多人誤解的地方。

class Value{
    int i =;
}
public class FinalTest {
    //編譯時常量,修飾的基本類型
    final int i1 =;
    //一般對象
    Value v1 = new Value();
    //被final修飾的對象
    final Value v2 = new Value();
    //final Value v4;  // 編譯報錯,沒有執行個體化

    //Arrays數組也是對象
    final int[] a = {, , , , };

    public static void main(String[] args) {
        FinalTest fd1 = new FinalTest();
        //fd1.i1++; // 編譯報錯不能改變值
        fd1.v2.i++; // 對象不是常量,對象本身可以改變
        fd1.v1 = new Value(); // 可以new對象 ---沒有被final修飾
        for(int i = ; i < fd1.a.length; i++)
        fd1.a[i]++; // 對象不是常量,本身可以改變
        //! fd1.v2 = new Value(); // 無法改變對象的指引
        //! fd1.a = new int[3];  //
    }
}
           

從 v1 到 v4 的變量向我們揭示出 final 修飾對象指引的含義。正如大家在 main()中看到的那樣,并不能認為由于 v2屬于 final,是以就不能再改變它的值(對象本身可以改變)。然而,我們确實不能再将 v2 綁定到一個新對象(不能改變指引指向另一個對象),因為它的屬性是final。這便是 final 對于一個句柄的确切含義。我們會發現同樣的含義亦适用于數組,後者隻不過是另一種類型的句柄而已。

空白 final

Java 1.1 允許我們建立“空白 final”,它們屬于一些特殊的字段。盡管被聲明成 final,但卻未得到一個初始值。無論在哪種情況下,空白 final 都必須在實際使用前得到正确的初始化。而且編譯器會主動保證這一規定得以貫徹。然而,對于 final 關鍵字的各種應用,空白 final 具有最大的靈活性。舉個例子來說,位于類内部的一個 final 字段現在對每個對象都可以有所不同,同時依然保持其“不變”的本質。下面列出一個例子:

public class BlankFinal {
    final int i = ; // 
    final int j; // 空白 final
    final Poppet p; // 空白 final指引

    // 空白 finals 必須被初始化
    BlankFinal() {
        j = ; 
        p = new Poppet();
    }

    public static void main(String[] args) {
        BlankFinal bf = new BlankFinal();
    }

}
           

final 自變量

我們可以将自變量設成 final 屬性,方法是在自變量清單中對它們進行适當的聲明。這意味着在一

個方法的内部,我們不能改變自變量句柄指向的東西。如下所示:

public class FinalArguments {
    void with(final Gizmo g) {
        // ! g = new Gizmo(); // 參數被final修飾
        g.spin();
    }

    void without(Gizmo g) {
        g = new Gizmo(); // OK -- g not final
        g.spin();
    }

    // void f(final int i) { i++; } // 不能改變
    // 可以讀取被自變量修飾的值:
    int g(final int i) {
        return i + ;
    }

    public static void main(String[] args) {
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
    }
}
           

注意:此時仍然能為 final 自變量配置設定一個 null(空)句柄,同時編譯器不會捕獲它。這與我們對非 final 自變量采取的操作是一樣的。方法 f()和 g()向我們展示出基本類型的自變量為 final 時會發生什麼情況:我們隻能讀取自變量,不可改變它。

final方法

之是以要使用 final 方法,可能是出于對兩方面理由的考慮。

第一個是為方法“上鎖”,防止任何繼承類改變它的本來含義。設計程式時,若希望一個方法的行為在繼承期間保持不變,而且不可被覆寫或改寫,就可以采取這種做法(當然我們也可以采用private修飾父類方法達到同樣的目的)。

第二個理由是程式執行的效率。将一個方法設成 final 後,編譯器就可以把對那個方法的所有調用都置入“嵌入”調用裡。隻要編譯器發現一個 final 方法調用,就會(根據它自己的判斷)忽略為執行方法調用機制而采取的正常代碼插入方法(将自變量壓入堆棧;跳至方法代碼并執行它;跳回來;清除堆棧自變量;最後對傳回值進行處理)。相反,它會用方法主體内實際代碼的一個副本來替換方法調用。這樣做可避免方法調用時的系統開銷。當然,若方法體積太大,那麼程式也會變得雍腫,可能感覺不到嵌入代碼所帶來的任何性能提升。因為任何提升都被花在方法内部的時間抵消了。 Java 編譯器能自動偵測這些情況,并頗為“明智”地決定是否嵌入一個 final 方法。然而,最好還是不要完全相信編譯器能正确地作出所有判斷。通常,隻有在方法的代碼量非常少,或者想明确禁止方法被覆寫的時候,才應考慮将一個方法設為final。

注意:類内所有 private 方法都自動成為 final。由于我們不能通路一個 private 方法,是以它絕對不會被其他方法覆寫(若強行這樣做,編譯器會給出錯誤提示)。我們也可手動為一個 private 方法添加 final 訓示符,但這樣多此一舉。

final 類

假如我們的類肯定不需要進行任何改變;或者出于安全方面的理由,我們不希望進行子類化(被繼承)。或者我們還考慮到執行效率的問題,并想確定涉及這個類各對象的所有行動都要盡可能地有效。我們就可以将這個類修飾成final。比如我常用的String類,Math類等等,都是final類。

當一個類被修飾為final,結果隻是禁止進行繼承—— 沒有更多的限制。它的資料成員可以是final也可以不是,這個由我們自己選擇。然而,由于它禁止了繼承,是以一個 final 類中的所有方法都預設為 final。因為此時再也無法覆寫它們。