跳至主要内容

7.3 Java final 關鍵字詳解及應用

  • 在不同環境中,Java 的關鍵字 final 含意存在細微的區別,通常指的是無法改變,可能出於兩種理由
    • 設計
    • 效率

final data

  • 許多語言都有某種方法告知編譯器某塊數據是恆定不變
    • 永不改變的編譯時常數
      • 運行時被初始化的值,而不希望被改變
  • Java 的常數必須是 primitives,並且以關鍵字 final 表示,定義時必須對其賦值
    • 既是 static 又是 final 的值表示佔據一段不能改變的儲存空間
  • final 運用在物件引用而非 primitives 時,final 使其引用恆定不變,即被初始化指向一個物件後,就不能再指向另一個物件,但物件自身是可以被修改的
    • Java 並未提供使任何物件恆定不變的途徑 (除了自己實現)
    • 這個限制同樣適用於陣列,因為它也是物件
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];
print(fd1);
print("Creating new FinalData");
FinalData fd2 = new FinalData("fd2");
print(fd1);
print(fd2);
}
} /* Output:
fd1: i4 = 15, INT_5 = 18
Creating new FinalData
fd1: i4 = 15, INT_5 = 18
fd2: i4 = 13, INT_5 = 18
*/
  • valueOneVALUE_TWO 都是帶有編譯時數值的基本類型,因此都可以用作編譯時常數
  • VALUE_THREE,定義為 publicstaticfinal ,是可以被外部使用的常數
  • 不能因為 final 定義就認為編譯期間就能知道其值,i4INT_5 都是執行期間才透過隨機數初始化
  • v2 由於是引用,final 意旨不能指向另一個物件,但仍可以改變內部的值

空白final

  • Java 允許生成「空白 final」,即聲明為 final 但沒有給初始值的欄位
  • 編譯器仍舊會確保空白 final 在使用前被初始化,空白 final 讓使用上具有更大的靈活性
class Poppet {
private int i;
Poppet(int ii) { i = ii; }
}

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) {
new BlankFinal();
new BlankFinal(47);
}
}

final 參數 (final arguments)

  • Java 允許在參數列表中將參數指定為 final,意味著無法在方法內更改參數引用所指向的對象
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);
}
}

final 方法

  • 使用 final 方法的原因有兩個:
    • 鎖定方法以防任何繼承類別修改它
    • 效率考量,早期的 Java 如果將方法宣告為 final 則表示同意編譯器將所有針對該方法的調用轉為內聯(inline)調用
      • 最近版本的 JVM 已經可以探測並優化去掉因濫用內聯而導致效率下降的調用
      • 在使用 Java SE5/6 時,應交由編譯器和 JVM 去處理效率問題,只在明確禁止覆蓋時才使用 final
  • class 中的 private 方法都是隱式的 final,由於無法取用所以也無法覆蓋
class WithFinals {
// Identical to "private" alone:
private final void f() { print("WithFinals.f()"); }
// Also automatically "final":
private void g() { print("WithFinals.g()"); }
}

class OverridingPrivate extends WithFinals {
private final void f() {
print("OverridingPrivate.f()");
}
private void g() {
print("OverridingPrivate.g()");
}
}

class OverridingPrivate2 extends OverridingPrivate {
public final void f() {
print("OverridingPrivate2.f()");
}
public void g() {
print("OverridingPrivate2.g()");
}
}

public class FinalOverridingIllusion {
public static void main(String[] args) {
OverridingPrivate2 op2 = new OverridingPrivate2();
op2.f();
op2.g();
// You can upcast:
OverridingPrivate op = op2;
// But you can't call the methods:
//! op.f();
//! op.g();
// Same here:
WithFinals wf = op2;
//! wf.f();
//! wf.g();
}
} /* Output:
OverridingPrivate2.f()
OverridingPrivate2.g()
*/
信息

“覆蓋” 只有在某方法是 base class 的接口的一部分才會出現,當某方法是 private 時並不會被視為接口的一部分,因此在衍生類別以相同名稱定義 publicprotected 或 package access 的方法並不算覆蓋該方法

final 類別

  • 將整個 class 定義為 final 時,就表明不打算繼承該類別,也不允許別人這麼做,這可能是出於某種考慮(或許是安全性考量),該 class 的設計永不需要變動,也不希望有子類
  • final class 的 field 可以自由選擇是或不是 final,然而,由於 final 類別禁止繼承,所以 class 內所有方法都是隱式地指定為 final
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++;
}
}

繼承類別的初始化及載入 (Initialization and class loading)

  • 許多傳統語言程式的初始化必須小心控制,例如在 C++ 中,如果 static 希望在初始化之前使用另一個 static,就會出現問題
  • Java 中 “class 的程式碼在初次使用的時候才加載”,但是當訪問 static field 或 method 時也會發生加載
    • 初次使用時便是 static 初始化發生之時
    • 所有 static 物件及程式碼片段都會在加載時按順序(定義時書寫的順序)初始化,並且 static 成員只會初始化一次

繼承與初始化

class Insect {
private int i = 9;
protected int j;
Insect() {
print("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 =
printInit("static Insect.x1 initialized");
static int printInit(String s) {
print(s);
return 47;
}
}

public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle() {
print("k = " + k);
print("j = " + j);
}
private static int x2 =
printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
print("Beetle constructor");
Beetle b = new Beetle();
}
} /* Output:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/
  • 運行步驟
    1. 試圖訪問 Beetle.main(),於是加載器開始啟動並找出 Beetle 的編譯程式碼 (Beetle.class)
    2. 加載的過程中發現它有一個基類(由 extends 表示),此時無論是否產生基類的對象,都會繼續加載基類
      1. 如果該基類還有其他基類,就會繼續往上加載
    3. 根基類中的 static 初始化(此處為 Insect)被執行,然後是下一個衍生類別,這種初始化順序很重要,因為衍生類別的 static 可能會依賴基類成員能否正確被初始化,到這邊所有必要的類別都已經加載完畢,對象可以被建立了
    4. 物件中的所有基本類型被設為預設值,物件引用被設為 null
    5. 基類的 constructor 被調用(本例為自動調用,也可使用 super 指定調用的建構子)
    6. 衍生類別的建構子按一樣的順序經歷相同的過程,最後衍生類別建構子的其餘部分被執行