上一篇我們提到關鍵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。因為此時再也無法覆寫它們。